# Get All Agent Executions Source: https://docs.unpod.ai/api/agent/get-agent-executions GET /api/v2/platform/agents/{agent_handle}/executions/ Retrieve all executions (call runs) for a specific agent ```json Success Response (200) theme={null} { "count": 12, "status_code": 200, "message": "Executions fetched successfully", "data": [ { "run_id": "Recac64fe03e911f1878d43cd8a99e069", "status": "completed", "total_tasks": 25, "completed_tasks": 23, "created": "2026-02-07T05:57:45Z" } ] } ``` ```json Error Response (404) theme={null} { "status_code": 404, "message": "Agent not found." } ``` # Get All Agent Executions Retrieve all execution runs associated with a specific agent. This endpoint provides a summary of each run including status, task counts, and timestamps - useful for monitoring agent performance and execution history. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. ### Path Parameters | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------------- | | agent\_handle | string | Yes | Unique handle/identifier of the agent | You can get the `agent_handle` by hitting the [Get All Agents](/api/agent/get-all-agents) API. The `handle` field in the response is your Agent Handle. ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Org-Handle | string | Yes | Organization domain handle | You can get the `Org-Handle` by hitting the [Get All Organizations](/api/space/organizations) API. The `domain_handle` field in the response is your Org-Handle. ### Response Fields | Field | Type | Description | | ------------ | ------- | -------------------------- | | count | integer | Total number of executions | | status\_code | integer | HTTP status code | | message | string | Response message | | data | array | Array of execution objects | ### Execution Object Fields | Field | Type | Description | | ---------------- | ------- | ------------------------------------------------------- | | run\_id | string | Unique run identifier | | status | string | Run status: `completed`, `running`, `failed`, `pending` | | total\_tasks | integer | Total number of tasks in the run | | completed\_tasks | integer | Number of successfully completed tasks | | created | string | Run creation timestamp (ISO 8601) | ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------- | | 200 | Success - Request completed successfully | | 400 | Bad Request - Invalid parameters provided | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Access denied to the resource | | 404 | Not Found - Agent not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' }; // Get all executions for an agent const getAgentExecutions = async (agentHandle) => { const response = await axios.get( `https://unpod.ai/api/v2/platform/agents/${agentHandle}/executions/`, { headers } ); console.log(`Total executions: ${response.data.count}`); response.data.data.forEach(exec => { const completionRate = (exec.completed_tasks / exec.total_tasks * 100).toFixed(1); console.log(`Run ${exec.run_id}: ${exec.status} (${completionRate}% complete)`); }); return response.data.data; }; // Example usage getAgentExecutions('space-agent-8qmk42nslp91wrh3dz7btxc4'); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' } def get_agent_executions(agent_handle: str): """Get all executions for a specific agent""" url = f'https://unpod.ai/api/v2/platform/agents/{agent_handle}/executions/' response = requests.get(url, headers=headers) data = response.json() print(f"Total executions: {data['count']}") for exec_item in data['data']: completion_rate = exec_item['completed_tasks'] / exec_item['total_tasks'] * 100 print(f"Run {exec_item['run_id']}: {exec_item['status']} ({completion_rate:.1f}% complete)") return data['data'] # Example usage get_agent_executions('space-agent-8qmk42nslp91wrh3dz7btxc4') ``` ```bash cURL theme={null} # Get all executions for an agent curl -X GET "https://unpod.ai/api/v2/platform/agents/space-agent-8qmk42nslp91wrh3dz7btxc4/executions/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" ``` ## Best Practices 1. **Agent Handle**: Use the exact agent handle from the [Get All Agents](/api/agent/get-all-agents) response 2. **Completion Tracking**: Calculate `completed_tasks / total_tasks` ratio to monitor run success rates 3. **Status Monitoring**: Poll this endpoint to track long-running executions 4. **Error Handling**: Always handle potential errors and edge cases 5. **Security**: Keep API tokens secure and rotate them regularly # Get All Agents Source: https://docs.unpod.ai/api/agent/get-all-agents GET /api/v2/platform/agents/ Retrieve all AI agents configured in your organization ```json Success Response (200) theme={null} { "count": 94, "status_code": 200, "message": "Agents fetched successfully", "data": [ { "handle": "space-agent-8qmk42nslp91wrh3dz7btxc4", "name": "General Agentic", "type": "Voice", "state": "published", "purpose": "Handle outbound sales calls" } ] } ``` ```json Error Response (401) theme={null} { "status_code": 401, "message": "Authentication credentials were not provided." } ``` # Get All Agents Retrieve detailed information for all registered AI agents in your organization, helping you view and manage agent records within the system. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. *** ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Org-Handle | string | Yes | Organization domain handle | You can get the `Org-Handle` by hitting the [Get All Organizations](/api/space/organizations) API. The `domain_handle` field in the response is your Org-Handle. ### Response Fields | Field | Type | Description | | ------------ | ------- | ---------------------- | | count | integer | Total number of agents | | status\_code | integer | HTTP status code | | message | string | Response message | | data | array | Array of agent objects | ### Agent Object Fields | Field | Type | Description | | ------- | ------ | --------------------------------------------- | | handle | string | Unique agent handle/identifier | | name | string | Agent display name | | type | string | Agent type: `Voice`, `Chat`, etc. | | state | string | Agent state: `published`, `draft`, `archived` | | purpose | string | Agent purpose/use case description | ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------- | | 200 | Success - Request completed successfully | | 400 | Bad Request - Invalid parameters provided | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Access denied to the resource | | 404 | Not Found - Organization not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' }; // Get all agents const getAllAgents = async () => { const response = await axios.get( 'https://unpod.ai/api/v2/platform/agents/', { headers } ); console.log(`Total agents: ${response.data.count}`); response.data.data.forEach(agent => { console.log(`${agent.name} (${agent.handle}) - ${agent.state}`); }); return response.data.data; }; // Example usage getAllAgents(); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' } def get_all_agents(): """Get all AI agents in the organization""" url = 'https://unpod.ai/api/v2/platform/agents/' response = requests.get(url, headers=headers) data = response.json() print(f"Total agents: {data['count']}") for agent in data['data']: print(f"{agent['name']} ({agent['handle']}) - {agent['state']}") return data['data'] # Example usage get_all_agents() ``` ```bash cURL theme={null} # Get all agents curl -X GET "https://unpod.ai/api/v2/platform/agents/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" ``` ## Best Practices 1. **Agent Handle**: Note the `handle` field - it is used as the `agent_handle` path parameter in other agent endpoints 2. **State Filtering**: Filter by `state` in your application to show only `published` agents 3. **Org-Handle**: Always include the correct organization handle in requests 4. **Error Handling**: Always handle potential errors and edge cases 5. **Security**: Keep API tokens secure and rotate them regularly # Get Tasks by Agent Handle Source: https://docs.unpod.ai/api/agent/get-tasks-by-agent-handle GET /api/v2/platform/agents/{agent_handle}/tasks/ Retrieve all tasks assigned to a specific agent using its handle ```json Success Response (200) theme={null} { "count": 1, "status_code": 200, "message": "Tasks Fetched Successfully", "data": [ { "_id": "697debb84c27faa892bfa0cc", "thread_id": "thread_9xkL2mQpR7vNwY4sZ3cJ8hF1", "user_info": { "email": "user@example.com", "full_name": "John Doe" }, "task_id": "T8ff2ccdffe9a11f0878d43cd8a99e069", "run_id": "R8ff2ccdefe9a11f0878d43cd8a99e069", "task": { "objective": "Call the lead and discuss the project requirements." }, "input": { "name": "John Doe", "contact_number": "1234567890", "email": "john@example.com", "context": "Follow up on proposal" }, "output": { "call_id": "CALL_7dAb3kR9mXvQ2pLw", "call_end_reason": "caller_hangup", "start_time": "2026-02-07T05:57:45Z", "end_time": "2026-02-07T06:02:30Z", "assistant_number": "+911234567890", "call_summary": "The agent successfully connected with John Doe and discussed project requirements. Lead expressed interest.", "duration": 285, "recording_url": "https://cdn.unpod.ai/recordings/CALL_7dAb3kR9mXvQ2pLw.mp3", "transcript": [ { "role": "agent", "content": "Hello, this is an AI assistant calling on behalf of Unpod. Am I speaking with John Doe?" }, { "role": "user", "content": "Yes, this is John." } ], "post_call_data": { "summary": "The agent successfully connected with John Doe and discussed project requirements.", "outcome": "interested", "sentiment": "positive" }, "call_type": "outbound", "call_status": "completed" }, "attachments": [], "assignee": "space-agent-8qmk42nslp91wrh3dz7btxc4", "status": "completed", "execution_type": "call", "ref_id": "6902fb5840a736e125e80ebc", "failure_count": 0, "last_failure_reason": null, "retry_attempt": 0, "last_status_change": "2026-02-07T06:02:35Z", "scheduled_timestamp": null } ] } ``` ```json Error Response (404) theme={null} { "message": "Agent not found" } ``` # Get Tasks by Agent Handle Fetch all tasks assigned to a specific agent/pilot using their handle. This returns full task metadata including user info, input payloads, call output, transcripts, and execution details. **Prerequisites:** Make sure you have your API Token ready. See [Authentication](/api/get-started/authentication) for details. ### Path Parameters | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------------- | | agent\_handle | string | Yes | Unique handle/identifier of the agent | You can get the `agent_handle` by hitting the [Get All Agents](/api/agent/get-all-agents) API. The `handle` field in the response is your Agent Handle. ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Content-Type | string | Yes | `application/json` | ``` ``` ### Response Fields | Field | Type | Description | | ------------ | ------- | --------------------- | | count | integer | Total number of tasks | | status\_code | integer | HTTP status code | | message | string | Response message | | data | array | Array of task objects | ### Task Object Fields | Field | Type | Description | | --------------------- | ------- | --------------------------------------------- | | \_id | string | Internal task document ID | | thread\_id | string | Thread identifier | | user\_info | object | User details (email, full\_name) | | task\_id | string | Unique task identifier | | run\_id | string | Parent run identifier | | task | object | Task definition with objective | | input | object | Input data for the task | | output | object | Task output with call details | | attachments | array | List of attachments | | assignee | string | Agent handle assigned to the task | | status | string | Task status: `pending`, `completed`, `failed` | | execution\_type | string | Type of execution: `call`, `email`, etc. | | ref\_id | string | Reference ID linking to input data | | failure\_count | integer | Number of failed attempts | | last\_failure\_reason | string | Reason for last failure (null if none) | | retry\_attempt | integer | Current retry attempt number | | last\_status\_change | string | Timestamp of last status change | | scheduled\_timestamp | string | Scheduled execution timestamp (null if now) | ### Output Object Fields | Field | Type | Description | | ----------------- | ------ | ---------------------------------------- | | call\_id | string | Unique call identifier | | call\_end\_reason | string | Reason for call ending | | start\_time | string | Call start timestamp | | end\_time | string | Call end timestamp | | assistant\_number | string | Outbound phone number used | | call\_summary | string | AI-generated summary of the call | | duration | number | Call duration in seconds | | recording\_url | string | URL to call recording | | transcript | array | Array of conversation messages | | post\_call\_data | object | Post-call analysis data | | call\_type | string | Direction of call: `outbound`, `inbound` | | call\_status | string | Status of the call: `completed`, etc. | ## Common Error Codes | Status Code | Description | | ----------- | ----------------------------------------------- | | 200 | Success - Data fetched successfully | | 206 | Partial Content - Business logic error occurred | | 400 | Bad Request - Invalid parameters provided | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Access denied to the resource | | 404 | Not Found - Agent not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Content-Type': 'application/json' }; // Get tasks by agent handle const getTasksByAgentHandle = async (agentHandle) => { const response = await axios.get( `https://unpod.ai/api/v2/platform/agents/${agentHandle}/tasks/`, { headers } ); console.log(`Total tasks: ${response.data.count}`); response.data.data.forEach(task => { console.log(`Task ${task.task_id}: ${task.status} (${task.execution_type})`); }); return response.data.data; }; // Example usage getTasksByAgentHandle('space-agent-8qmk42nslp91wrh3dz7btxc4'); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Content-Type': 'application/json' } def get_tasks_by_agent_handle(agent_handle: str): """Get all tasks assigned to a specific agent""" url = f'https://unpod.ai/api/v2/platform/agents/{agent_handle}/tasks/' response = requests.get(url, headers=headers) data = response.json() print(f"Total tasks: {data['count']}") for task in data['data']: print(f"Task {task['task_id']}: {task['status']} ({task['execution_type']})") return data['data'] # Example usage get_tasks_by_agent_handle('space-agent-8qmk42nslp91wrh3dz7btxc4') ``` ```bash cURL theme={null} # Get tasks by agent handle curl -X GET "https://unpod.ai/api/v2/platform/agents/space-agent-8qmk42nslp91wrh3dz7btxc4/tasks/" \ -H "Authorization: Token your-api-token" \ -H "Content-Type: application/json" ``` ## Best Practices 1. **Agent Handle**: Use the exact agent handle from the [Get All Agents](/api/agent/get-all-agents) response 2. **Transcript Analysis**: Use the `transcript` field in the output for detailed conversation analysis 3. **Retry Logic**: Check `failure_count` and `last_failure_reason` for failed tasks to understand issues 4. **Recording Access**: Store `recording_url` from output for compliance and quality review 5. **Security**: Keep API tokens secure and rotate them regularly # Overview Source: https://docs.unpod.ai/api/agent/overview Introduction to the Agent API # Agent API The Agent API allows you to manage and retrieve AI agents configured in your Unpod platform. Agents are voice AI entities that handle calls, follow personas, and execute tasks. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. ## What You Can Do | Endpoint | Description | | -------------------------------------------------------- | ----------------------------------------------- | | `GET /api/v2/platform/agents/` | Retrieve all agents in your organization | | `GET /api/v2/platform/agents/{agent_handle}/tasks/` | Retrieve all tasks assigned to a specific agent | | `GET /api/v2/platform/agents/{agent_handle}/executions/` | Retrieve all executions for a specific agent | ## Base URL ``` https://unpod.ai/api/v2/platform/ ``` ## Authentication All Agent API requests require a valid API token and Org-Handle in the headers: ```http theme={null} Authorization: Token your-api-token Org-Handle: your-org-handle ``` ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------- | | 200 | Success - Request completed successfully | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Access denied to the resource | | 404 | Not Found - Agent not found | | 500 | Internal Server Error - Server-side error | # Overview Source: https://docs.unpod.ai/api/billing&usage/analytics-overview Introduction to the Analytics API # Analytics API The Analytics API lets you fetch call analytics and task status for your organization. It provides insights into call outcomes, success rates, and task execution summaries. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. ## What You Can Do | Endpoint | Description | | ---------------------------------------------- | ------------------------------------------------------------ | | `GET /api/v2/platform/organisation/analytics/` | Retrieve call analytics and task status for the organization | ## Base URL ``` https://unpod.ai/api/v2/platform/ ``` ## Authentication All Analytics API requests require a valid token and Org-Handle in the headers: ```http theme={null} Authorization: jwt Org-Handle: your-org-handle ``` ## Behavior * If `space_token` is provided → fetches analytics for that specific space * If `space_token` is not provided → fetches analytics for **all contact spaces** in the organization * Automatically filters for **contact content type** only For billing/invoice data, use the separate [Billing](/api/billing\&usage/billing-overview) endpoint. ## Common Error Codes | Status Code | Description | | ----------- | ---------------------------------------- | | 200 | Success - Analytics fetched successfully | | 400 | Bad Request - Missing required headers | | 401 | Unauthorized - Invalid or missing token | | 404 | Not Found - Organization not found | # Overview Source: https://docs.unpod.ai/api/billing&usage/billing-overview Introduction to the Billing API # Billing API The Billing API lets you fetch invoices and payment status for your organization. It provides a paginated list of subscription invoices with their payment details. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. ## What You Can Do | Endpoint | Description | | -------------------------------------------- | ------------------------------------------------------- | | `GET /api/v2/platform/organisation/billing/` | Retrieve invoices and billing data for the organization | ## Base URL ``` https://unpod.ai/api/v2/platform/ ``` ## Authentication All Billing API requests require a valid JWT token and Org-Handle in the headers: ```http theme={null} Authorization: jwt Org-Handle: your-org-handle ``` For call analytics data, use the separate [Analytics](/api/billing\&usage/get-analytics) endpoint. ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------- | | 200 | Success - Billing data fetched successfully | | 400 | Bad Request - Missing required headers | | 401 | Unauthorized - Invalid or missing token | | 404 | Not Found - Organization not found | # Get Analytics Source: https://docs.unpod.ai/api/billing&usage/get-analytics GET /api/v2/platform/organisation/analytics/ Retrieve call analytics and task status for your organization ```json Success Response (200) theme={null} { "status_code": 200, "message": "Analytics fetched successfully", "data": { "total_calls": 1250, "completed_calls": 987, "failed_calls": 143, "avg_duration_seconds": 145.3, "total_tasks": 1350, "task_status_breakdown": { "pending": 45, "running": 12, "completed": 1180, "failed": 113 } } } ``` ```json Error Response (401) theme={null} { "status_code": 401, "message": "Authentication credentials were not provided." } ``` # Get Analytics Retrieve call analytics and task status for your organization. Provides insights into call outcomes, success rates, average durations, and task execution summaries. Optionally filter by a specific space using the `space_token` query parameter. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Org-Handle | string | Yes | Organization domain handle | You can get the `Org-Handle` by hitting the [Get All Organizations](/api/space/organizations) API. The `domain_handle` field in the response is your Org-Handle. ### Query Parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------------------------------------------------------------------------- | | space\_token | string | No | Filter analytics for a specific space. If omitted, returns org-wide analytics | If `space_token` is provided, analytics are scoped to that space only. If not provided, analytics aggregate across all contact spaces in the organization. ### Response Fields | Field | Type | Description | | ------------ | ------- | ---------------- | | status\_code | integer | HTTP status code | | message | string | Response message | | data | object | Analytics data | ### Analytics Data Fields | Field | Type | Description | | ----------------------- | ------- | -------------------------------------- | | total\_calls | integer | Total number of calls made | | completed\_calls | integer | Number of successfully completed calls | | failed\_calls | integer | Number of failed calls | | avg\_duration\_seconds | number | Average call duration in seconds | | total\_tasks | integer | Total number of tasks across all runs | | task\_status\_breakdown | object | Breakdown of tasks by status | ### Task Status Breakdown Fields | Field | Type | Description | | --------- | ------- | -------------------------------------- | | pending | integer | Number of tasks awaiting execution | | running | integer | Number of tasks currently executing | | completed | integer | Number of successfully completed tasks | | failed | integer | Number of failed tasks | *** ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------- | | 200 | Success - Analytics fetched successfully | | 400 | Bad Request - Missing required headers | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Invalid organization handle | | 404 | Not Found - Organization not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' }; // Get org-wide analytics const getAnalytics = async (spaceToken = null) => { const params = spaceToken ? { space_token: spaceToken } : {}; const response = await axios.get( 'https://unpod.ai/api/v2/platform/organisation/analytics/', { headers, params } ); const data = response.data.data; console.log(`Total calls: ${data.total_calls}`); console.log(`Completed: ${data.completed_calls}`); console.log(`Failed: ${data.failed_calls}`); console.log(`Avg duration: ${data.avg_duration_seconds}s`); return data; }; // Get org-wide analytics getAnalytics(); // Get analytics for a specific space getAnalytics('8KZAMRAHSXXXXXXMAYNASMJC'); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' } def get_analytics(space_token: str = None): """Get call analytics and task status""" url = 'https://unpod.ai/api/v2/platform/organisation/analytics/' params = {'space_token': space_token} if space_token else {} response = requests.get(url, headers=headers, params=params) data = response.json()['data'] print(f"Total calls: {data['total_calls']}") print(f"Completed: {data['completed_calls']}") print(f"Failed: {data['failed_calls']}") print(f"Avg duration: {data['avg_duration_seconds']}s") return data # Get org-wide analytics get_analytics() # Get analytics for a specific space get_analytics('8KZAMRAHSXXXXXXMAYNASMJC') ``` ```bash cURL theme={null} # Get org-wide analytics curl -X GET "https://unpod.ai/api/v2/platform/organisation/analytics/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" # Get analytics for a specific space curl -X GET "https://unpod.ai/api/v2/platform/organisation/analytics/?space_token=8KZAMRAHSXXXXXXMAYNASMJC" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" ``` ## Best Practices 1. **Space Scoping**: Use the `space_token` filter to get analytics for individual campaigns or spaces 2. **Success Rate**: Calculate success rate as `completed_calls / total_calls * 100` for performance tracking 3. **Task Breakdown**: Monitor `task_status_breakdown` to detect stuck or failed tasks requiring attention 4. **Scheduled Reports**: Call this endpoint on a schedule to build trend dashboards 5. **Org-Handle**: Ensure the correct organization handle is included for accurate scoping 6. **Security**: Keep API tokens secure and rotate them regularly # Get Billing Data Source: https://docs.unpod.ai/api/billing&usage/get-billing GET /api/v2/platform/organisation/billing/ Retrieve invoices and payment status for your organization ```json Success Response (200) theme={null} { "count": 12, "status_code": 200, "message": "Billing data fetched successfully", "results": [ { "id": 101, "amount": "299.00", "currency": "USD", "status": "paid", "invoice_date": "2026-02-01T00:00:00Z" } ] } ``` ```json Error Response (401) theme={null} { "status_code": 401, "message": "Authentication credentials were not provided." } ``` # Get Billing Data Retrieve invoices and payment history for your organization with pagination support. This endpoint returns billing records including invoice amounts, currency, payment status, and invoice dates. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Org-Handle | string | Yes | Organization domain handle | You can get the `Org-Handle` by hitting the [Get All Organizations](/api/space/organizations) API. The `domain_handle` field in the response is your Org-Handle. ### Query Parameters | Name | Type | Required | Description | | ---------- | ------- | -------- | ----------------------------------------- | | page | integer | No | Page number for pagination (default = 1) | | page\_size | integer | No | Number of records per page (default = 20) | ### Response Fields | Field | Type | Description | | ------------ | ------- | -------------------------------- | | count | integer | Total number of billing records | | status\_code | integer | HTTP status code | | message | string | Response message | | results | array | Array of invoice/billing objects | ### Invoice Object Fields | Field | Type | Description | | ------------- | ------- | ------------------------------------------- | | id | integer | Unique invoice identifier | | amount | string | Invoice amount as decimal string | | currency | string | Currency code (e.g., `USD`, `INR`) | | status | string | Payment status: `paid`, `pending`, `failed` | | invoice\_date | string | Invoice creation date (ISO 8601) | ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------- | | 200 | Success - Billing data fetched successfully | | 400 | Bad Request - Invalid parameters provided | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Invalid organization handle | | 404 | Not Found - Organization not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' }; // Get billing data with pagination const getBillingData = async (page = 1, pageSize = 20) => { const response = await axios.get( 'https://unpod.ai/api/v2/platform/organisation/billing/', { headers, params: { page, page_size: pageSize } } ); console.log(`Total invoices: ${response.data.count}`); response.data.results.forEach(invoice => { console.log(`Invoice #${invoice.id}: ${invoice.currency} ${invoice.amount} - ${invoice.status}`); }); return response.data.results; }; // Example usage getBillingData(1, 20); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' } def get_billing_data(page: int = 1, page_size: int = 20): """Get billing invoices with pagination""" url = 'https://unpod.ai/api/v2/platform/organisation/billing/' params = {'page': page, 'page_size': page_size} response = requests.get(url, headers=headers, params=params) data = response.json() print(f"Total invoices: {data['count']}") for invoice in data['results']: print(f"Invoice #{invoice['id']}: {invoice['currency']} {invoice['amount']} - {invoice['status']}") return data['results'] # Example usage get_billing_data(page=1, page_size=20) ``` ```bash cURL theme={null} # Get billing data curl -X GET "https://unpod.ai/api/v2/platform/organisation/billing/?page=1&page_size=20" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" ``` ## Best Practices 1. **Pagination**: Use `page` and `page_size` to retrieve billing records in manageable chunks 2. **Status Monitoring**: Watch for `pending` or `failed` invoice statuses that may require action 3. **Currency Handling**: Always check the `currency` field before displaying amounts to users 4. **Org-Handle**: Ensure the correct organization handle is used for accurate billing records 5. **Reconciliation**: Use `invoice_date` alongside `id` for billing reconciliation workflows 6. **Security**: Keep API tokens secure and rotate them regularly # API Changelog Source: https://docs.unpod.ai/api/changelog Version history, breaking changes, and migration notes for the Unpod REST API **Last updated:** July 22, 2026 · **Current API version:** v2 · **Source of truth:** `openapi.yaml` 2.0.0 This page tracks changes to the Unpod REST API. The current production API is **v2**, served from `https://unpod.ai/` under the `/api/v2/platform/` path prefix with `Token` authentication. ## Conventions | Item | Value | | --------------- | ----------------------------------------------------------------------------- | | Production host | `https://unpod.ai/` | | Path prefix | `/api/v2/platform/` | | Authentication | `Authorization: Token ` | | Common headers | `Org-Handle` (many endpoints), `Product-ID`, `Content-Type: application/json` | *** ## v2 — Current **Status:** Active · Production * Production host standardized to `https://unpod.ai/`. * All endpoints served under `/api/v2/platform/`. * Authentication uses the `Token` scheme: `Authorization: Token `. * Task creation is space-scoped: `POST /api/v2/platform/spaces/{space_token}/tasks/create/`, taking a `pilot` handle and a `documents` array of contacts. * Call detail records exposed at `/api/v2/platform/cdr/`. * Telephony numbers are read-only via the API (`GET /api/v2/platform/telephony/numbers/`); provisioning happens through the connected SIP provider / Dashboard. See the [Quickstart](/api/get-started/quickstart) and [Authentication](/api/get-started/authentication) for full details. *** ## v1 — Deprecated **Status:** Deprecated · Do not use for new integrations The legacy `https://api.unpod.ai/api/v1/` host and `Authorization: Bearer` scheme are deprecated. Migrate to v2 (see below). ### v1 → v2 migration | Concern | v1 (deprecated) | v2 (current) | | ------------------ | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | Host | `https://api.unpod.ai/` | `https://unpod.ai/` | | Path prefix | `/api/v1/` | `/api/v2/platform/` | | Auth header | `Authorization: Bearer ` | `Authorization: Token ` | | Org scoping | implicit | `Org-Handle` header | | List organizations | `GET /api/v1/organizations/` | `GET /api/v2/platform/organizations/` | | List spaces | `GET /api/v1/spaces/` | `GET /api/v2/platform/spaces/` | | List agents | `GET /api/v1/agents/` | `GET /api/v2/platform/agents/` | | Create task | `POST /api/v1/tasks/` (flat `agent_handle`/`phone_number` body) | `POST /api/v2/platform/spaces/{space_token}/tasks/create/` (`pilot` + `documents[]` body) | | List runs | `GET /api/v1/runs/` | `GET /api/v2/platform/spaces/{space_token}/runs/` | | Call logs | `GET /api/v1/call-logs/` | `GET /api/v2/platform/cdr/` | | Create bridge | `POST /api/v1/bridges/` | `POST /api/v2/platform/telephony/bridges/` (`name` + `slug`) | | Update bridge | `PATCH /api/v1/bridges/{slug}/` | `PATCH /api/v2/platform/telephony/bridges/{slug}/` | The self-hosted open-source stack exposes its own internal services under `/api/v1/` on `localhost:8000`. That is a separate surface from the hosted cloud platform API documented here — see [Self-Hosting](/platform/self-hosting/architecture). # Create a Task - Make Voice AI Call Source: https://docs.unpod.ai/api/execution/create-task POST /api/v2/platform/spaces/{space_token}/tasks/create/ Create a new task in a space to trigger a Voice AI outbound call ```bash cURL theme={null} curl --request POST \ --url https://unpod.ai/api/v2/platform/spaces/{space_token}/tasks/create/ \ --header 'Authorization: Token ' \ --header 'Content-Type: application/json' \ --header 'Org-Handle: ' \ --data '{ "pilot": "space-agent-f1o3qjm1y7q1avvuynv4vprb1", "context": "Call the lead and discuss the project requirements.", "schedule": { "type": "now" }, "documents": [ { "name": "John Doe", "email": "john@example.com", "contact_number": "1234567890", "alternate_number": "+919876543210", "occupation": "Sales Manager", "company_name": "Acme Corp", "address": "123 Main St, Mumbai", "about": "Warm lead from webinar", "context": "Follow up on proposal sent last week", "labels": ["warm-lead", "webinar"], "title": "Q1 Outreach", "description": "Lead from Q1 campaign", "document_id": "6902fb5840a736e125e80ebc" } ] }' ``` ```json Success Response (200) theme={null} { "status_code": 200, "message": "Task Created Successfully", "data": { "run_id": "R74802366fe9011f0878d43cd8a99e069", "task_ids": [ "T74802367fe9011f0878d43cd8a99e069" ], "status": "pending" } } ``` ```json Error Response (206) theme={null} { "message": "Task creation failed", "errors": "Detailed error description" } ``` # Create a Task - Make Voice AI Call Create a new task inside a given space using the space's public token. You can assign the task to a pilot/agent, attach multiple documents (contacts), provide additional context, and schedule the execution. **Prerequisites:** Make sure you have your API Token, Space Token, and Agent Handle ready. See [Authentication](/api/get-started/authentication) for details. ### Endpoint ``` POST /api/v2/platform/spaces/{space_token}/tasks/create/ ``` ### Path Parameters Public token of the space where the task is created. Get this from the [Get All Spaces](/api/space/get-all-spaces) API - use the `token` field. ### Headers API Key in format: `Token ` Must be `application/json` ### Request Body Agent/pilot handle to assign the task to. Array of document/contact objects. Each document represents one contact to call. Contact name Primary contact phone number Contact email address Alternate contact phone number Contact's occupation Contact's company name Contact's address Additional info about the contact Per-contact context for the call Labels/tags associated with the record Document title Document description Unique document identifier Additional context/objective for the task (applies to all contacts). Schedule configuration. Example: `{"type": "now"}` ### Response Fields HTTP status code Response message Created task details Created run identifier - use this to track execution via [Get All Runs](/api/execution/get-all-runs) Array of created task identifiers (one per document/contact) Task status - initially `pending` ## Common Error Codes | Status Code | Description | | ----------- | ----------------------------------------------- | | 200 | Success - Task created successfully | | 206 | Partial Content - Business logic error occurred | | 400 | Bad Request - Invalid parameters provided | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Access denied to the resource | | 404 | Not Found - Space or Agent not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Content-Type': 'application/json' }; // Create a task to make a Voice AI call const createTask = async (spaceToken, pilot, documents, context) => { const response = await axios.post( `https://unpod.ai/api/v2/platform/spaces/${spaceToken}/tasks/create/`, { pilot, documents, context, schedule: { type: 'now' } }, { headers } ); console.log(`Run ID: ${response.data.data.run_id}`); console.log(`Task IDs: ${response.data.data.task_ids.join(', ')}`); return response.data.data; }; // Example usage createTask( '8KZAMRAHSXXXXXXMAYNASMJC', 'space-agent-f1o3qjm1y7q1avvuynv4vprb1', [ { name: 'John Doe', contact_number: '1234567890', email: 'john@example.com', context: 'Follow up on proposal' } ], 'Call the lead and discuss the project requirements.' ); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Content-Type': 'application/json' } def create_task(space_token: str, pilot: str, documents: list, context: str): """Create a Voice AI call task in a space""" url = f'https://unpod.ai/api/v2/platform/spaces/{space_token}/tasks/create/' payload = { 'pilot': pilot, 'documents': documents, 'context': context, 'schedule': {'type': 'now'} } response = requests.post(url, headers=headers, json=payload) data = response.json() print(f"Run ID: {data['data']['run_id']}") print(f"Task IDs: {', '.join(data['data']['task_ids'])}") return data['data'] # Example usage create_task( space_token='8KZAMRAHSXXXXXXMAYNASMJC', pilot='space-agent-f1o3qjm1y7q1avvuynv4vprb1', documents=[ { 'name': 'John Doe', 'contact_number': '1234567890', 'email': 'john@example.com', 'context': 'Follow up on proposal' } ], context='Call the lead and discuss the project requirements.' ) ``` ```bash cURL theme={null} # Create a task to trigger a Voice AI call curl -X POST "https://unpod.ai/api/v2/platform/spaces/8KZAMRAHSXXXXXXMAYNASMJC/tasks/create/" \ -H "Authorization: Token your-api-token" \ -H "Content-Type: application/json" \ -d '{ "pilot": "space-agent-f1o3qjm1y7q1avvuynv4vprb1", "documents": [ { "name": "John Doe", "contact_number": "1234567890", "email": "john@example.com", "context": "Follow up on proposal", "document_id": "6902fb5840a736e125e80ebc" } ], "context": "Call the lead and discuss the project requirements.", "schedule": {"type": "now"} }' ``` ## Best Practices 1. **Pilot Handle**: Use the correct agent handle from the [Get All Agents](/api/agent/get-all-agents) API 2. **contact\_number**: Always include a valid `contact_number` in each document - this is required for call execution 3. **Batch Calls**: Pass multiple documents in the `documents` array to trigger batch calls in a single request 4. **Context**: Provide clear, specific context to guide the agent's conversation objectives 5. **Run ID**: Store the returned `run_id` to track task execution status using the [Get All Runs](/api/execution/get-all-runs) API 6. **Security**: Keep API tokens secure and rotate them regularly # Get All Runs in a Space Source: https://docs.unpod.ai/api/execution/get-all-runs GET /api/v2/platform/spaces/{space_token}/runs/ Retrieve all batch executions associated with a specific space ```json Success Response (200) theme={null} { "count": 9, "status_code": 200, "message": "Runs Fetched Successfully", "data": [ { "run_id": "Recac64fe03e911f1878d43cd8a99e069", "collection_ref": "collection_data_SA2W8R6NZRK6C9PO3JSL1J85", "run_mode": "prefect", "status": "completed", "created": "2026-02-07T05:57:45", "modified": "2026-02-07T05:57:45" } ] } ``` ```json Error Response (206) theme={null} { "message": "Error fetching runs", "errors": "Detailed error description" } ``` # Get All Runs in a Space Retrieve all runs associated with a specific space. A run represents a batch execution - a group of tasks triggered together within a space. This endpoint returns run details including run ID, status, timestamps, and associated collection metadata. **Prerequisites:** Make sure you have your API Token ready. See [Authentication](/api/get-started/authentication) for details. *** ### Path Parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ---------------------------------- | | space\_token | string | Yes | Public token identifying the space | You can get the `space_token` by hitting the [Get All Spaces](/api/space/get-all-spaces) API. The `token` field in the response is your Space Token. ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Content-Type | string | Yes | `application/json` | ### Response Fields | Field | Type | Description | | ------------ | ------- | -------------------- | | count | integer | Total number of runs | | status\_code | integer | HTTP status code | | message | string | Response message | | data | array | Array of run objects | ### Run Object Fields | Field | Type | Description | | --------------- | ------ | ------------------------------------------------------- | | run\_id | string | Unique run identifier | | collection\_ref | string | Reference to the data collection | | run\_mode | string | Execution engine: `prefect`, etc. | | status | string | Run status: `completed`, `running`, `failed`, `pending` | | created | string | Run creation timestamp | | modified | string | Last modified timestamp | ## Common Error Codes | Status Code | Description | | ----------- | ----------------------------------------------- | | 200 | Success - Data fetched successfully | | 206 | Partial Content - Business logic error occurred | | 400 | Bad Request - Invalid parameters provided | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Access denied to the resource | | 404 | Not Found - Space not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Content-Type': 'application/json' }; // Get all runs in a space const getAllRuns = async (spaceToken) => { const response = await axios.get( `https://unpod.ai/api/v2/platform/spaces/${spaceToken}/runs/`, { headers } ); console.log(`Total runs: ${response.data.count}`); response.data.data.forEach(run => { console.log(`Run ${run.run_id}: ${run.status} (${run.run_mode})`); }); return response.data.data; }; // Example usage getAllRuns('8KZAMRAHSXXXXXXMAYNASMJC'); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Content-Type': 'application/json' } def get_all_runs(space_token: str): """Get all runs in a space""" url = f'https://unpod.ai/api/v2/platform/spaces/{space_token}/runs/' response = requests.get(url, headers=headers) data = response.json() print(f"Total runs: {data['count']}") for run in data['data']: print(f"Run {run['run_id']}: {run['status']} ({run['run_mode']})") return data['data'] # Example usage get_all_runs('8KZAMRAHSXXXXXXMAYNASMJC') ``` ```bash cURL theme={null} # Get all runs in a space curl -X GET "https://unpod.ai/api/v2/platform/spaces/8KZAMRAHSXXXXXXMAYNASMJC/runs/" \ -H "Authorization: Token your-api-token" \ -H "Content-Type: application/json" ``` ## Best Practices 1. **Space Token**: Always use the correct space token for your API requests 2. **Run ID**: Store the `run_id` from each run to query tasks within that run 3. **Status Monitoring**: Poll this endpoint to track the status of ongoing batch executions 4. **Error Handling**: Always handle potential errors and edge cases 5. **Security**: Keep API tokens secure and rotate them regularly # Get Detailed Tasks for a Specific Run Source: https://docs.unpod.ai/api/execution/get-run-tasks GET /api/v2/platform/spaces/{space_token}/runs/{run_id}/tasks/ Retrieve all tasks with full execution details for a specific run ```json Success Response (200) theme={null} { "count": 9, "status_code": 200, "message": "Tasks Fetched Successfully", "data": [ { "_id": "697debb84c27faa892bfa0cc", "thread_id": "thread_9xkL2mQpR7vNwY4sZ3cJ8hF1", "user_info": { "email": "user@example.com", "full_name": "John Doe" }, "task_id": "T8ff2ccdffe9a11f0878d43cd8a99e069", "run_id": "Recac64fe03e911f1878d43cd8a99e069", "task": { "objective": "Call the lead and discuss the project requirements." }, "input": { "name": "John Doe", "contact_number": "1234567890", "email": "john@example.com", "context": "Follow up on proposal" }, "output": { "call_id": "CALL_7dAb3kR9mXvQ2pLw", "start_time": "2026-02-07T05:57:45Z", "end_time": "2026-02-07T06:02:30Z", "duration": 285, "recording_url": "https://cdn.unpod.ai/recordings/CALL_7dAb3kR9mXvQ2pLw.mp3", "transcript": [ { "role": "agent", "content": "Hello, this is an AI assistant calling on behalf of Unpod. Am I speaking with John Doe?" }, { "role": "user", "content": "Yes, this is John." } ], "post_call_data": { "summary": "The agent successfully connected with John Doe and discussed project requirements.", "outcome": "interested", "sentiment": "positive" }, "call_type": "outbound", "call_status": "completed" }, "assignee": "space-agent-8qmk42nslp91wrh3dz7btxc4", "status": "completed", "execution_type": "call", "created": "2026-02-07T05:57:45Z", "modified": "2026-02-07T06:02:35Z" } ] } ``` ```json Error Response (206) theme={null} { "message": "Error retrieving run tasks", "errors": "Detailed error description" } ``` # Get Detailed Tasks for a Specific Run Fetch all tasks for a specific run, including complete input, output, call transcript, costs, analysis, artifacts, and provider metadata. This endpoint is used when you need deep inspection of how a run executed each task. **Prerequisites:** Make sure you have your API Token ready. See [Authentication](/api/get-started/authentication) for details. *** ### Path Parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ---------------------------------- | | space\_token | string | Yes | Public token of the space | | run\_id | string | Yes | The run ID whose tasks to retrieve | You can get the `space_token` from the [Get All Spaces](/api/space/get-all-spaces) API. The `run_id` can be obtained from the [Get All Runs](/api/execution/get-all-runs) API response. ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Content-Type | string | Yes | `application/json` | ### Response Fields | Field | Type | Description | | ------------ | ------- | --------------------- | | count | integer | Total number of tasks | | status\_code | integer | HTTP status code | | message | string | Response message | | data | array | Array of task objects | ### Task Object Fields | Field | Type | Description | | --------------- | ------ | --------------------------------------------- | | \_id | string | Internal task document ID | | thread\_id | string | Thread identifier | | user\_info | object | User details (email, full\_name) | | task\_id | string | Unique task identifier | | run\_id | string | Parent run identifier | | task | object | Task definition with objective | | input | object | Input data for the task | | output | object | Task output including call details | | assignee | string | Agent handle assigned to the task | | status | string | Task status: `pending`, `completed`, `failed` | | execution\_type | string | Type of execution: `call`, `email`, etc. | | created | string | Task creation timestamp | | modified | string | Last modified timestamp | ### Output Object Fields | Field | Type | Description | | ---------------- | ------ | -------------------------------- | | call\_id | string | Unique call identifier | | start\_time | string | Call start timestamp | | end\_time | string | Call end timestamp | | duration | number | Call duration in seconds | | recording\_url | string | URL to call recording | | transcript | array | Array of conversation messages | | post\_call\_data | object | Post-call analysis data | | call\_type | string | Direction: `outbound`, `inbound` | | call\_status | string | Status of the call | *** ## Common Error Codes | Status Code | Description | | ----------- | ----------------------------------------------- | | 200 | Success - Data fetched successfully | | 206 | Partial Content - Business logic error occurred | | 400 | Bad Request - Invalid parameters provided | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Access denied to the resource | | 404 | Not Found - Space or Run not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Content-Type': 'application/json' }; // Get tasks for a specific run const getRunTasks = async (spaceToken, runId) => { const response = await axios.get( `https://unpod.ai/api/v2/platform/spaces/${spaceToken}/runs/${runId}/tasks/`, { headers } ); console.log(`Total tasks: ${response.data.count}`); response.data.data.forEach(task => { console.log(`Task ${task.task_id}: ${task.status}`); if (task.output?.duration) { console.log(` Duration: ${task.output.duration}s`); } }); return response.data.data; }; // Example usage getRunTasks('8KZAMRAHSXXXXXXMAYNASMJC', 'Recac64fe03e911f1878d43cd8a99e069'); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Content-Type': 'application/json' } def get_run_tasks(space_token: str, run_id: str): """Get all tasks for a specific run""" url = f'https://unpod.ai/api/v2/platform/spaces/{space_token}/runs/{run_id}/tasks/' response = requests.get(url, headers=headers) data = response.json() print(f"Total tasks: {data['count']}") for task in data['data']: print(f"Task {task['task_id']}: {task['status']}") if task.get('output', {}).get('duration'): print(f" Duration: {task['output']['duration']}s") return data['data'] # Example usage get_run_tasks('8KZAMRAHSXXXXXXMAYNASMJC', 'Recac64fe03e911f1878d43cd8a99e069') ``` ```bash cURL theme={null} # Get tasks for a specific run curl -X GET "https://unpod.ai/api/v2/platform/spaces/8KZAMRAHSXXXXXXMAYNASMJC/runs/Recac64fe03e911f1878d43cd8a99e069/tasks/" \ -H "Authorization: Token your-api-token" \ -H "Content-Type: application/json" ``` ## Best Practices 1. **Run ID**: Always retrieve the `run_id` from the [Get All Runs](/api/execution/get-all-runs) endpoint first 2. **Transcript Review**: Use the `transcript` array in output for conversation quality analysis 3. **Recording Access**: Store `recording_url` for compliance, QA, and playback 4. **Post-call Data**: Check `post_call_data.summary` for AI-generated call summaries and outcomes 5. **Error Handling**: Always handle potential errors and edge cases 6. **Security**: Keep API tokens secure and rotate them regularly # Overview Source: https://docs.unpod.ai/api/execution/runs-overview Introduction to the Runs & Executions API # Runs & Executions API The Runs API allows you to manage and monitor execution runs in your Unpod platform. A **run** represents a batch execution - a group of tasks triggered together within a space. **Prerequisites:** Make sure you have your API Token. See [Authentication](/api/get-started/authentication) for details. ## What You Can Do | Endpoint | Description | | ---------------------------------------------------------------- | ------------------------------------- | | `GET /api/v2/platform/spaces/{space_token}/runs/` | Retrieve all batch runs in a space | | `GET /api/v2/platform/spaces/{space_token}/runs/{run_id}/tasks/` | Retrieve all tasks for a specific run | ## Run Lifecycle Animated platform execution lifecycle diagram showing task creation, pending state, execution, and completed or failed outcomes. ## Base URL ``` https://unpod.ai/api/v2/platform/ ``` ## Authentication All Runs API requests require a valid API token in the headers: ```http theme={null} Authorization: Token your-api-token Content-Type: application/json ``` ## Common Error Codes | Status Code | Description | | ----------- | ----------------------------------------------- | | 200 | Success - Data fetched successfully | | 206 | Partial Content - Business logic error occurred | | 401 | Unauthorized - Invalid or missing API token | | 404 | Not Found - Space or Run not found | | 500 | Internal Server Error - Server-side error | # Overview Source: https://docs.unpod.ai/api/execution/task-make-call-overview Introduction to the Task - Make Call API # Task - Make Call API The Task - Make Call API lets you create Voice AI outbound call tasks in a specific space. Each task triggers a call workflow for one or more contacts and is executed by a selected agent/pilot. **Prerequisites:** Make sure you have your API Token, Space Token, and Agent Handle ready. See [Authentication](/api/get-started/authentication) for details. ## What You Can Do | Endpoint | Description | | ---------------------------------------------------------- | ---------------------------------------------------- | | `POST /api/v2/platform/spaces/{space_token}/tasks/create/` | Create a Voice AI call task for one or more contacts | ## Call Task Flow Animated platform execution lifecycle diagram showing task creation, pending state, execution, and completed or failed outcomes. ## Base URL ``` https://unpod.ai/api/v2/platform/ ``` ## Authentication All Task - Make Call API requests require a valid API token in the headers: ```http theme={null} Authorization: Token your-api-token Content-Type: application/json ``` ## Common Error Codes | Status Code | Description | | ----------- | ----------------------------------------------- | | 200 | Success - Task created successfully | | 206 | Partial Content - Business logic error occurred | | 400 | Bad Request - Invalid input payload | | 401 | Unauthorized - Invalid or missing API token | | 404 | Not Found - Space not found | | 500 | Internal Server Error - Server-side error | # Authentication Source: https://docs.unpod.ai/api/get-started/authentication Manage authentication for API access ## Overview The Unpod SDK needs an access token to connect to the server successfully. This token holds the participant's identity, room name, capabilities, and permissions. Tokens are signed with your API secret to block forgery, and include an expiration time after which the server rejects them. Expiration time only impacts the initial connection, and not subsequent reconnects. ## Authentication Method The Unpod API uses **API Key Authentication**: | Header | Format | Example | | ------------- | --------------- | ---------------------------------------------- | | Authorization | `Token ` | `Authorization: Token a1b2c3d4e5f6g7h8i9j0...` | ## How to Get Your API Token Follow these steps to obtain your API token from the Unpod Dashboard: 1. **Login** to the [Unpod Dashboard](https://unpod.ai/) 2. After login, you'll be redirected to the **Hub** page 3. On the **left sidebar**, click on the **Key** icon (Api Keys) 4. You'll be redirected to the **API Keys** page 5. Click **Generate New API Key** 6. Copy and securely store your API token If you delete an API key, you can always generate a new one from the API Keys page. ## Error Responses ### 401 - Unauthorized Returned when authentication credentials are missing or invalid. ```json theme={null} { "status_code": 401, "message": "Authentication credentials were not provided." } ``` ### 403 - Forbidden Returned when the token is expired or access is denied. ```json theme={null} { "status_code": 403, "message": "Token has expired." } ``` ### 400 - Bad Request Returned when required headers are missing. ```json theme={null} { "status_code": 400, "message": "Org-Handle header is required." } ``` ## How to Get Your Organization Handle Follow these steps to obtain your Organization Handle from the Unpod Dashboard: 1. **Login** to the [Unpod Dashboard](https://unpod.ai/) 2. On the **left sidebar**, click on the **Studio View** 3. On the **right sidebar**, click on the **Setting Icon** 4. You will see the **Copy Org Handle** option 5. Click on it to **copy** the Organization Handle The Organization Handle is required in the `Org-Handle` header for many API endpoints. ## How to Get Your Space Token Follow these steps to obtain your Space Token from the Unpod Dashboard: 1. **Login** to the [Unpod Dashboard](https://unpod.ai/) 2. On the **left sidebar**, click on the **Conversation** tab 3. Click on the **Settings** icon 4. You'll see the **Space Token** option 5. Click on it to **copy** the Space Token Each space has a unique token. Make sure you're using the correct space token for your API requests. ## How to Get Your Agent Handle Follow these steps to obtain your Agent Handle from the Unpod Dashboard: 1. **Login** to the [Unpod Dashboard](https://unpod.ai/) 2. On the **left sidebar**, click on the **Studio View** 3. Click on **Create AI Identity**, you will be redirected to the Agents page 4. Click on the **agent** for which you want the Agent Handle 5. On the **right sidebar**, you will see **three dots** behind Publish, click on that 6. You will see the **Copy Handle** option 7. Click on it to **copy** the Agent Handle Each agent has a unique handle. Make sure you're using the correct agent handle for your API requests. ## Best Practices 1. **Secure Storage**: Never expose your API tokens in client-side code or public repositories 2. **HTTPS Only**: Always use HTTPS for all API requests 3. **Header Validation**: Always include required headers (Org-Handle, Product-ID) where needed 4. **Error Handling**: Implement proper error handling for authentication failures 5. **Rotation**: Rotate API tokens periodically for enhanced security # Quickstart Source: https://docs.unpod.ai/api/get-started/quickstart Get started with Unpod using the Dashboard or API ## Overview Unpod provides two ways to interact with the platform: | Method | Description | Best For | | ------------- | ------------------------------------------------ | ------------------------------------------ | | **Dashboard** | Visual interface at [unpod.ai](https://unpod.ai) | Manual setup, configuration, monitoring | | **REST API** | Programmatic access | Automation, integrations, custom workflows | ## Understand Core Components Unpod has 4 core building blocks: | Component | Purpose | | ------------- | ----------------------------------------------------- | | **Numbers** | Virtual phone numbers (entry points for calls) | | **Providers** | SIP trunking services (LiveKit, Vapi, Twilio) | | **Bridges** | Routing hubs that connect Numbers, Providers & Agents | | **Agents** | AI voice agents that handle conversations | Animated core component data flow diagram showing a number routed through a bridge to AI agents, then through a carrier provider to an endpoint. ## API Setup ### Base URL ``` https://unpod.ai/ ``` ### Authentication Include this header in all requests: ```http theme={null} Authorization: Token your-api-token ``` ### Required Headers | Header | Required | Description | | ------------- | -------------- | -------------------------- | | Authorization | Yes | `Token ` | | Org-Handle | Sometimes | Organization domain handle | | Product-ID | Sometimes | Product identifier | | Content-Type | For POST/PATCH | `application/json` | ## Quick API Examples ### List All Bridges ```bash theme={null} curl -X GET "https://unpod.ai/api/v2/platform/telephony/bridges/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" \ -H "Product-ID: your-product-id" ``` ### Create a Bridge ```bash theme={null} curl -X POST "https://unpod.ai/api/v2/platform/telephony/bridges/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" \ -H "Content-Type: application/json" \ -d '{ "name": "My Bridge", "description": "Production bridge", "region": "IN" }' ``` ### Get Call Detail Records (CDR) ```bash theme={null} curl -X GET "https://unpod.ai/api/v2/platform/cdr/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" ``` ### Get Providers ```bash theme={null} curl -X GET "https://unpod.ai/api/v2/platform/telephony/providers/" \ -H "Authorization: Token your-api-token" ``` *** ## Next Steps * [Authentication](/api/get-started/authentication) - Learn about authentication methods * [Bridges API](/api/telephony/bridges-overview) - Full bridges documentation * [Numbers API](/api/telephony/numbers) - Manage phone numbers * [Provider Configurations](/api/provider/overview) - Configure providers *** ## Need Help? * [Discord Community](https://discord.gg/7kQ4ewNSHZ) * [YouTube Channel](https://www.youtube.com/@unpod-ai) * [GitHub](https://github.com/unpod-ai/unpod) # Get Call Detail Records (CDR) Source: https://docs.unpod.ai/api/logs/get-call-logs GET /api/v2/platform/cdr/ Retrieve telephony call detail records with filtering and pagination support ```json Success Response (200) theme={null} { "count": 643, "status_code": 200, "message": "Call logs fetched successfully", "data": [ { "id": 33364, "call_status": "completed", "end_reason": "call.in-progress.sip-completed-call", "call_type": "outbound", "bridge": { "id": 12, "name": "Acme Primary Bridge" }, "creation_time": "2025-11-08T05:32:29Z", "start_time": "2025-11-08T05:29:43Z", "end_time": "2025-11-08T05:32:28.686639Z", "call_duration": 165.686639, "source_number": "+15551234567", "destination_number": "+15559876543", "failure_source": null, "sip_cause": null } ] } ``` ```json Error Response (401) theme={null} { "status_code": 401, "message": "Authentication credentials were not provided." } ``` # Get Call Detail Records (CDR) Retrieve telephony call detail records (CDR) for your organization - only SIP-based telephony calls, not voice agent calls. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. *** ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Org-Handle | string | Yes | Organization domain handle | | Authorization | string | Yes | API Key format: `Token ` | You can get the `Org-Handle` by hitting the [Get All Organizations](/api/space/organizations) API. The `domain_handle` field in the response is your Org-Handle. ### Query Parameters | Name | Type | Required | Description | | ------------ | ------- | -------- | ---------------------------------------------------------- | | page | integer | No | Page number for pagination (default = 1) | | page\_size | integer | No | Number of records per page (default = 20) | | call\_type | string | No | Filter by call direction: `inbound` or `outbound` | | call\_status | string | No | Filter by status: `completed`, `notConnected`, or `failed` | ### Response Fields | Field | Type | Description | | ------------ | ------- | -------------------- | | count | integer | Total number of CDRs | | status\_code | integer | HTTP status code | | message | string | Response message | | data | array | Array of CDR objects | ### CDR Object Fields | Field | Type | Description | | ------------------- | ------- | --------------------------------------------------- | | id | integer | Unique call record ID | | source\_number | string | The number that initiated the call | | destination\_number | string | The number that received the call | | call\_type | string | Direction of call: `outbound`, `inbound` | | call\_status | string | Outcome: `completed`, `notConnected`, `failed` | | bridge | object | `{ id, name }` of the bridge, or `null` | | creation\_time | string | Timestamp when the call was created/scheduled | | start\_time | string | Timestamp when the call was answered | | end\_time | string | Timestamp when the call ended | | call\_duration | number | Duration of the call in seconds | | end\_reason | string | Reason the call ended | | failure\_source | string | Which side a failure originated on (`null` if none) | | sip\_cause | string | Human-readable SIP cause (`null` if none) | This CDR surface returns **telephony SIP records only** - it intentionally omits agent / space / organization attribution (voice-agent call logs live elsewhere). *** ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------- | | 200 | Success - CDRs fetched successfully | | 400 | Bad Request - Invalid filter parameters | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Invalid organization handle | | 404 | Not Found - Organization not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' }; // Get CDRs with filters const getCDRs = async (filters = {}) => { const params = { page: 1, page_size: 20, ...filters }; const response = await axios.get( 'https://unpod.ai/api/v2/platform/cdr/', { headers, params } ); console.log(`Total CDRs: ${response.data.count}`); response.data.data.forEach(log => { console.log(`${log.call_type} call to ${log.destination_number}: ${log.call_status} (${log.call_duration}s)`); }); return response.data.data; }; // Example: Get outbound completed calls getCDRs({ call_type: 'outbound', call_status: 'completed' }); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' } def get_cdrs(page: int = 1, page_size: int = 20, **filters): """Get CDRs with optional filters""" url = 'https://unpod.ai/api/v2/platform/cdr/' params = {'page': page, 'page_size': page_size, **filters} response = requests.get(url, headers=headers, params=params) data = response.json() print(f"Total CDRs: {data['count']}") for log in data['data']: print(f"{log['call_type']} to {log['destination_number']}: {log['call_status']} ({log['call_duration']}s)") return data['data'] # Example: Get outbound completed calls get_cdrs( page=1, page_size=20, call_type='outbound', call_status='completed' ) ``` ```bash cURL theme={null} # Get CDRs with filters curl -X GET "https://unpod.ai/api/v2/platform/cdr/?page=1&page_size=20&call_type=outbound&call_status=completed" \ -H "Org-Handle: your-org-handle" \ -H "Authorization: Token your-api-token" ``` ## Best Practices 1. **Pagination**: Use `page` and `page_size` for large datasets to avoid timeouts 2. **Call Status Filtering**: Filter by `call_status` to focus on specific outcomes 3. **Org-Handle**: Ensure the correct organization handle is included 4. **Security**: Keep API tokens secure and rotate them regularly # Overview Source: https://docs.unpod.ai/api/logs/overview Introduction to the Call Detail Records (CDR) API # Call Detail Records (CDR) API The CDR API allows you to retrieve telephony call detail records for your organization. It provides comprehensive call details including duration, status, timestamps, and source/destination numbers. **Note:** This API only returns SIP-based telephony calls (`product_types: telephony_sip`), not voice agent calls. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. ## What You Can Do | Endpoint | Description | | --------------------------- | ------------------------------------------------------- | | `GET /api/v2/platform/cdr/` | Retrieve all telephony CDRs with filtering & pagination | ## Base URL ``` https://unpod.ai/api/v2/platform/ ``` ## Authentication All CDR API requests require a valid API token and Org-Handle in the headers: ```http theme={null} Authorization: Token your-api-token Org-Handle: your-org-handle ``` ## Available Filters | Filter | Description | | ------------- | -------------------------------------------------- | | `page` | Page number for pagination | | `page_size` | Number of records per page | | `call_type` | Filter by `inbound` or `outbound` | | `call_status` | Filter by `completed`, `notConnected`, or `failed` | ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------- | | 200 | Success - Request completed successfully | | 400 | Bad Request - Invalid parameters provided | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Invalid organization handle | | 500 | Internal Server Error - Server-side error | # API Overview Source: https://docs.unpod.ai/api/overview Base URL, authentication, rate limiting, and error handling for the Unpod REST API - stated once. The Unpod REST API manages platform resources: spaces, agents, tasks, runs, call logs, telephony (bridges, numbers, providers), and billing. This page states the conventions once; the endpoint pages in the sidebar assume them. Building a voice agent in Python? You likely want the [Speech Stack](/get-started/quickstart) instead - the `unpod` SDK wraps provisioning and live-call handling. The REST API is for direct integrations and non-Python stacks. ## Base URL ``` https://unpod.ai/api/v2/platform ``` All endpoint paths in this reference are relative to it. ## Authentication Every request carries an API token in the `Authorization` header using the `Token` scheme: ```http theme={null} Authorization: Token a1b2c3d4e5f6g7h8i9j0... ``` Generate tokens from the [Unpod Dashboard](https://unpod.ai/api-keys/) - see [Authentication](/api/get-started/authentication) for the step-by-step, space-token scoping, and error responses. ```bash theme={null} curl -X GET "https://unpod.ai/api/v2/platform/telephony/bridges/" \ -H "Authorization: Token $UNPOD_TOKEN" ``` ## Rate limiting Responses include rate-limit headers; exceeding your limit returns `429 Too Many Requests`: | Header | Meaning | | ----------------------- | ------------------------------------- | | `X-RateLimit-Limit` | Max requests in the current window | | `X-RateLimit-Remaining` | Requests left in the window | | `X-RateLimit-Reset` | Unix timestamp when the window resets | | `Retry-After` | Seconds to wait (only on 429) | Handle 429s with exponential backoff, honoring `Retry-After`. Cache reads and batch writes where you can. Current limits depend on your plan - check the dashboard. ## Errors The API uses standard HTTP status codes. Error bodies are JSON with a machine-readable code and a human-readable message: ```json theme={null} { "error": { "code": "rate_limit_exceeded", "message": "Rate limit exceeded. Please try again in 60 seconds." } } ``` Common cases: `401` - token missing, expired, or access denied (see [Authentication](/api/get-started/authentication)); `404` - resource not found or outside your space; `429` - rate limited. ## Start here First authenticated requests against bridges, call logs, and providers. Tokens, space scoping, and auth errors in detail. # Webhook Integration: Receive Unpod Call Data in Your System Source: https://docs.unpod.ai/api/post-call-data Build a webhook receiver to capture Unpod call transcripts, summaries, and analysis data - then push it to your CRM, Slack, or database **Last updated:** July 22, 2026 · **API version:** v2 · **Tested against:** `openapi.yaml` 2.0.0 *** Every Unpod call produces rich structured data: full transcripts, AI-generated summaries, sentiment analysis, call recordings, and extracted lead intelligence. This guide shows you how to pull that data into your own system - whether that's a CRM, database, Slack, or a custom pipeline. ## What Data Is Available After a Call When an Unpod call completes, the task output contains: ```json theme={null} { "output": { "call_id": "675272289779974401", "call_type": "outbound", "call_status": "completed", "start_time": "2026-01-31 11:47:38.067021", "end_time": "2026-01-31 11:48:11.385745", "duration": 33, "recording_url": "https://storage.example.com/call.ogg", "transcript": [ { "role": "assistant", "content": "Hi, this is Aria from Acme...", "timestamp": "..." }, { "role": "user", "content": "Yes, I'm interested", "timestamp": "..." } ], "post_call_data": { "summary": { "status": "Connected", "summary": "Prospect expressed interest in the enterprise plan...", "callback_datetime": "2026-02-01T10:00:00" }, "profile_summary": { "tone": "Positive", "engagement": "High", "interest_level": "Very Interested", "outcome": "Meeting Scheduled", "next_action": "Send proposal email" }, "classification": { "labels": ["Interested", "Decision Maker"] } } } } ``` This is the data you want flowing into your business systems automatically. *** ## Integration Architecture Unpod uses a **poll + process** model. You poll the API to detect completed calls, then process and forward the data. Animated post-call data integration diagram showing Unpod API data flowing through a polling service into CRM, Slack, database, and custom webhook systems. For high-volume deployments, run the polling service as a background worker. For lower volume, a cron job works fine. *** ## Step 1: Set Up Your Webhook Receiver Build an HTTP endpoint that receives processed call data. Here's a minimal receiver in Node.js and Python: ```javascript Node.js (Express) theme={null} const express = require('express'); const app = express(); app.use(express.json()); app.post('/webhook/unpod-call', (req, res) => { const { task_id, run_id, status, output, input } = req.body; if (status !== 'completed') { return res.sendStatus(200); // ignore non-completed } const { call_status, duration, recording_url, transcript, post_call_data } = output; const summary = post_call_data?.summary; const profile = post_call_data?.profile_summary; console.log(`Call completed: ${task_id}`); console.log(`Contact: ${input.name} (${input.contact_number})`); console.log(`Duration: ${duration}s | Outcome: ${profile?.outcome}`); console.log(`Next action: ${profile?.next_action}`); // Process further: update CRM, send Slack, write to DB... processCallData({ task_id, input, summary, profile, recording_url, transcript }); res.sendStatus(200); }); app.listen(3000, () => console.log('Webhook receiver running on :3000')); ``` ```python Python (FastAPI) theme={null} from fastapi import FastAPI from pydantic import BaseModel from typing import Optional, Any app = FastAPI() class CallWebhookPayload(BaseModel): task_id: str run_id: str status: str input: dict output: Optional[dict] = None @app.post("/webhook/unpod-call") async def receive_call(payload: CallWebhookPayload): if payload.status != "completed": return {"ok": True} output = payload.output or {} post_call = output.get("post_call_data", {}) profile = post_call.get("profile_summary", {}) summary = post_call.get("summary", {}) print(f"Call completed: {payload.task_id}") print(f"Contact: {payload.input.get('name')} ({payload.input.get('contact_number')})") print(f"Outcome: {profile.get('outcome')}") print(f"Next action: {profile.get('next_action')}") # Process further await process_call_data(payload.task_id, payload.input, output, post_call) return {"ok": True} ``` *** ## Step 2: Build the Polling Service This service runs continuously, detects newly completed calls, and forwards data to your webhook receiver. ```javascript Node.js theme={null} const axios = require('axios'); const UNPOD_API = 'https://unpod.ai/api/v2/platform'; const API_TOKEN = process.env.UNPOD_API_TOKEN; const SPACE_TOKEN = process.env.UNPOD_SPACE_TOKEN; const WEBHOOK_URL = process.env.WEBHOOK_URL; // your receiver const headers = { Authorization: `Token ${API_TOKEN}`, 'Content-Type': 'application/json' }; // Track processed tasks to avoid duplicates const processedTasks = new Set(); async function fetchCompletedTasks(page = 1) { const res = await axios.get( `${UNPOD_API}/spaces/${SPACE_TOKEN}/tasks/?page=${page}&page_size=50`, { headers } ); return res.data.data; } async function processNewCompletedTasks() { const tasks = await fetchCompletedTasks(); for (const task of tasks) { if (task.status === 'completed' && !processedTasks.has(task.task_id)) { processedTasks.add(task.task_id); // Forward to your webhook receiver await axios.post(WEBHOOK_URL, { task_id: task.task_id, run_id: task.run_id, status: task.status, input: task.input, output: task.output }); console.log(`Forwarded task ${task.task_id} to webhook`); } } } // Poll every 30 seconds setInterval(processNewCompletedTasks, 30_000); processNewCompletedTasks(); // run immediately on start ``` ```python Python theme={null} import asyncio import httpx import os from datetime import datetime UNPOD_API = "https://unpod.ai/api/v2/platform" API_TOKEN = os.environ["UNPOD_API_TOKEN"] SPACE_TOKEN = os.environ["UNPOD_SPACE_TOKEN"] WEBHOOK_URL = os.environ["WEBHOOK_URL"] HEADERS = { "Authorization": f"Token {API_TOKEN}", "Content-Type": "application/json" } processed_tasks: set[str] = set() async def fetch_tasks(client: httpx.AsyncClient, page: int = 1) -> list[dict]: url = f"{UNPOD_API}/spaces/{SPACE_TOKEN}/tasks/" r = await client.get(url, params={"page": page, "page_size": 50}, headers=HEADERS) r.raise_for_status() return r.json()["data"] async def poll_and_forward(): async with httpx.AsyncClient() as client: while True: tasks = await fetch_tasks(client) for task in tasks: task_id = task["task_id"] if task["status"] == "completed" and task_id not in processed_tasks: processed_tasks.add(task_id) await client.post(WEBHOOK_URL, json={ "task_id": task_id, "run_id": task["run_id"], "status": task["status"], "input": task.get("input", {}), "output": task.get("output", {}) }) print(f"Forwarded task {task_id}") await asyncio.sleep(30) # poll every 30s if __name__ == "__main__": asyncio.run(poll_and_forward()) ``` *** ## Step 3: Integrate with Your Business Systems ### Push to HubSpot CRM After a call, update the contact record with the outcome and schedule follow-up: ```javascript theme={null} const hubspot = require('@hubspot/api-client'); const client = new hubspot.Client({ accessToken: process.env.HUBSPOT_TOKEN }); async function updateHubspotContact(input, profile, summary) { // Find contact by phone number const searchResult = await client.crm.contacts.searchApi.doSearch({ filterGroups: [{ filters: [{ propertyName: 'phone', operator: 'EQ', value: input.contact_number }] }], properties: ['firstname', 'phone', 'hs_lead_status'] }); if (!searchResult.results.length) return; const contactId = searchResult.results[0].id; // Update contact with call outcome await client.crm.contacts.basicApi.update(contactId, { properties: { hs_lead_status: mapOutcomeToHubspotStatus(profile.outcome), notes_last_contacted: new Date().toISOString(), hs_sales_email_last_replied: summary.callback_datetime || '', } }); // Create an activity note await client.crm.objects.notes.basicApi.create({ properties: { hs_note_body: `AI Call Summary:\n${summary.summary}\n\nTone: ${profile.tone}\nInterest: ${profile.interest_level}\nNext Action: ${profile.next_action}`, hs_timestamp: new Date().toISOString() }, associations: [{ to: { id: contactId }, types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 202 }] }] }); } function mapOutcomeToHubspotStatus(outcome) { const map = { 'Meeting Scheduled': 'IN_PROGRESS', 'Not Interested': 'UNQUALIFIED', 'Connected': 'OPEN', 'Not Connected': 'OPEN' }; return map[outcome] || 'OPEN'; } ``` ### Send Slack Notification Alert your sales team when a high-interest call completes: ```python theme={null} import httpx import os SLACK_WEBHOOK = os.environ["SLACK_WEBHOOK_URL"] async def notify_slack(input: dict, profile: dict, summary: dict, recording_url: str): interest = profile.get("interest_level", "Unknown") outcome = profile.get("outcome", "Unknown") next_action = profile.get("next_action", "Review call") # Only alert for high-interest calls if interest not in ("Very Interested", "Interested"): return emoji = "🔥" if interest == "Very Interested" else "✅" message = { "blocks": [ { "type": "header", "text": {"type": "plain_text", "text": f"{emoji} Hot Lead Call Completed"} }, { "type": "section", "fields": [ {"type": "mrkdwn", "text": f"*Contact:*\n{input.get('name')} ({input.get('contact_number')})"}, {"type": "mrkdwn", "text": f"*Company:*\n{input.get('company_name', 'N/A')}"}, {"type": "mrkdwn", "text": f"*Interest:*\n{interest}"}, {"type": "mrkdwn", "text": f"*Outcome:*\n{outcome}"} ] }, { "type": "section", "text": {"type": "mrkdwn", "text": f"*Summary:*\n{summary.get('summary', 'N/A')}"} }, { "type": "section", "text": {"type": "mrkdwn", "text": f"*Next Action:* {next_action}"} }, { "type": "actions", "elements": [ { "type": "button", "text": {"type": "plain_text", "text": "Listen to Recording"}, "url": recording_url } ] } ] } async with httpx.AsyncClient() as client: await client.post(SLACK_WEBHOOK, json=message) ``` ### Write to PostgreSQL Persist call data for analytics and reporting: ```python theme={null} import asyncpg import json from datetime import datetime async def save_call_to_db(pool: asyncpg.Pool, task: dict, output: dict, post_call: dict): profile = post_call.get("profile_summary", {}) summary = post_call.get("summary", {}) await pool.execute(""" INSERT INTO call_records ( task_id, run_id, contact_name, contact_phone, company, call_status, duration, outcome, interest_level, tone, next_action, summary_text, recording_url, transcript, created_at ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) ON CONFLICT (task_id) DO NOTHING """, task["task_id"], task["run_id"], task["input"].get("name"), task["input"].get("contact_number"), task["input"].get("company_name"), output.get("call_status"), output.get("duration"), profile.get("outcome"), profile.get("interest_level"), profile.get("tone"), profile.get("next_action"), summary.get("summary"), output.get("recording_url"), json.dumps(output.get("transcript", [])), datetime.utcnow() ) ``` *** ## Step 4: Handle Transcripts The transcript array gives you the full conversation: ```python theme={null} def extract_transcript_text(transcript: list[dict]) -> str: lines = [] for msg in transcript: role = "Agent" if msg["role"] == "assistant" else "Customer" lines.append(f"{role}: {msg['content']}") return "\n".join(lines) def get_call_duration_seconds(output: dict) -> int: return output.get("duration", 0) def was_call_answered(output: dict) -> bool: return output.get("call_status") == "completed" and output.get("duration", 0) > 5 ``` *** ## Production Checklist Before going live with your integration: * [ ] **Idempotency** - Use `task_id` as a deduplication key. Store processed IDs in Redis or DB to prevent double-processing * [ ] **Error handling** - Retry failed webhook deliveries with exponential backoff * [ ] **Persist cursor** - Store the last processed timestamp or task ID so restarts don't reprocess all history * [ ] **Secrets** - Store `UNPOD_API_TOKEN` in environment variables, not in code * [ ] **Rate limits** - Unpod API has rate limits; add delays between polling requests if processing large backlogs * [ ] **Logging** - Log every task ID processed for audit trail * [ ] **Alerting** - Alert if the polling service stops delivering (gap in processed timestamps) *** ## Quick Reference: Key Data Fields | Field | Path | Use Case | | -------------- | ------------------------------------------------------ | ------------------- | | Contact name | `input.name` | CRM lookup | | Phone number | `input.contact_number` | CRM lookup | | Call outcome | `output.post_call_data.profile_summary.outcome` | Lead status | | Interest level | `output.post_call_data.profile_summary.interest_level` | Prioritization | | Next action | `output.post_call_data.profile_summary.next_action` | Task creation | | Call summary | `output.post_call_data.summary.summary` | CRM note | | Callback time | `output.post_call_data.summary.callback_datetime` | Schedule follow-up | | Recording URL | `output.recording_url` | Audit / QA | | Transcript | `output.transcript[]` | Analysis / training | | Duration | `output.duration` | Billing / analytics | *** ## What's Next Trigger outbound calls programmatically. Fetch task results and call data. Query historical call logs. Aggregate usage and performance metrics. # Create Provider Source: https://docs.unpod.ai/api/provider/create-provider POST /api/v2/platform/telephony/providers-configurations/ Create a new telephony provider configuration ```json Success Response (201) theme={null} { "status_code": 201, "message": "Provider configuration created successfully.", "data": { "id": 42, "provider": "twilio", "account_sid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "created_at": "2026-02-07T10:00:00Z" } } ``` ```json Error Response (400) theme={null} { "status_code": 400, "message": "Provider configuration creation failed.", "errors": "Invalid account_sid or auth_token" } ``` # Create Provider Configuration Create a new telephony provider configuration by linking your provider credentials (Account SID and Auth Token) to your organization. These configurations are used to connect telephony providers to bridges. **Prerequisites:** Make sure you have your API Token, Org-Handle, and telephony provider credentials ready. See [Authentication](/api/get-started/authentication) for details. ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Org-Handle | string | Yes | Organization domain handle | | Content-Type | string | Yes | `application/json` | You can get the `Org-Handle` by hitting the [Get All Organizations](/api/space/organizations) API. The `domain_handle` field in the response is your Org-Handle. ### Request Body | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------- | | provider | string | Yes | Provider identifier (slug or ID from Get Providers API) | | account\_sid | string | Yes | Provider Account SID / Account identifier | | auth\_token | string | Yes | Provider Auth Token / Secret key | ``` ``` ### Response Fields | Field | Type | Description | | ------------ | ------- | ------------------------------- | | status\_code | integer | HTTP status code | | message | string | Response message | | data | object | Created provider config details | ### Provider Config Object Fields | Field | Type | Description | | ------------ | ------- | ----------------------------------------- | | id | integer | Unique provider configuration identifier | | provider | string | Provider slug/identifier | | account\_sid | string | Account SID (auth\_token is not returned) | | created\_at | string | Creation timestamp (ISO 8601) | ## Common Error Codes | Status Code | Description | | ----------- | ----------------------------------------------- | | 201 | Created - Provider configuration created | | 400 | Bad Request - Invalid credentials or parameters | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Invalid organization handle | | 409 | Conflict - Configuration already exists | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle', 'Content-Type': 'application/json' }; // Create a provider configuration const createProvider = async (provider, accountSid, authToken) => { const response = await axios.post( 'https://unpod.ai/api/v2/platform/telephony/providers-configurations/', { provider, account_sid: accountSid, auth_token: authToken }, { headers } ); console.log(`Provider created with ID: ${response.data.data.id}`); return response.data.data; }; // Example usage createProvider('twilio', 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'your-auth-token'); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle', 'Content-Type': 'application/json' } def create_provider(provider: str, account_sid: str, auth_token: str): """Create a new telephony provider configuration""" url = 'https://unpod.ai/api/v2/platform/telephony/providers-configurations/' payload = { 'provider': provider, 'account_sid': account_sid, 'auth_token': auth_token } response = requests.post(url, headers=headers, json=payload) data = response.json() print(f"Provider created with ID: {data['data']['id']}") return data['data'] # Example usage create_provider('twilio', 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'your-auth-token') ``` ```bash cURL theme={null} # Create a provider configuration curl -X POST "https://unpod.ai/api/v2/platform/telephony/providers-configurations/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" \ -H "Content-Type: application/json" \ -d '{ "provider": "twilio", "account_sid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "auth_token": "your-auth-token-here" }' ``` ## Best Practices 1. **Provider Slug**: Use the `slug` from the [Get Telephony Providers](/api/provider/get-telephony-providers) endpoint to identify the provider 2. **Credential Security**: Never log or expose `auth_token` values - treat them as secrets 3. **Credential Rotation**: Use the [Update Provider](/api/provider/update-provider) endpoint to rotate credentials without deleting the configuration 4. **Configuration ID**: Store the returned `id` to reference this configuration when connecting to bridges 5. **Error Handling**: Handle 400 errors that indicate invalid credentials before they cause production issues 6. **Security**: Keep API tokens secure and rotate them regularly # Delete Provider Source: https://docs.unpod.ai/api/provider/delete-provider DELETE /api/v2/platform/telephony/providers-configurations/{id}/ Permanently delete a telephony provider configuration ```json Success Response (204) theme={null} HTTP/1.1 204 No Content ``` ```json Error Response (404) theme={null} { "status_code": 404, "message": "Provider configuration not found." } ``` # Delete Provider Configuration Permanently delete a telephony provider configuration by its ID. Once deleted, this configuration can no longer be used to connect to bridges. This action cannot be undone. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. Deleting a provider configuration that is actively connected to a bridge may disrupt call routing. Disconnect the provider from all bridges before deleting. ### Path Parameters | Name | Type | Required | Description | | ---- | ------- | -------- | ----------------------------------------------- | | id | integer | Yes | Unique identifier of the provider configuration | You can get the provider configuration `id` by hitting the [Get All Providers](/api/provider/get-all-providers) API. ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Org-Handle | string | Yes | Organization domain handle | ## Common Error Codes | Status Code | Description | | ----------- | ----------------------------------------------- | | 204 | No Content - Configuration deleted successfully | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Invalid organization handle | | 404 | Not Found - Provider configuration not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' }; // Delete a provider configuration const deleteProvider = async (id) => { await axios.delete( `https://unpod.ai/api/v2/platform/telephony/providers-configurations/${id}/`, { headers } ); console.log(`Provider configuration #${id} deleted successfully`); }; // Example usage deleteProvider(42); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' } def delete_provider(config_id: int): """Delete a provider configuration""" url = f'https://unpod.ai/api/v2/platform/telephony/providers-configurations/{config_id}/' response = requests.delete(url, headers=headers) if response.status_code == 204: print(f"Provider configuration #{config_id} deleted successfully") else: print(f"Error: {response.json()}") # Example usage delete_provider(42) ``` ```bash cURL theme={null} # Delete a provider configuration curl -X DELETE "https://unpod.ai/api/v2/platform/telephony/providers-configurations/42/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" ``` ## Best Practices 1. **Disconnect First**: Always [disconnect the provider from bridges](/api/telephony/disconnect-provider-from-bridge) before deleting a configuration 2. **Irreversible**: Deletion is permanent - verify the correct `id` before proceeding 3. **Audit Trail**: Note the deletion in your records for compliance and troubleshooting purposes 4. **204 Response**: A successful delete returns HTTP 204 with no body - handle this in your code 5. **Error Handling**: Handle 404 gracefully - the configuration may have already been deleted # Get All Providers Source: https://docs.unpod.ai/api/provider/get-all-providers GET /api/v2/platform/telephony/providers-configurations/ Retrieve all configured telephony provider configurations ```json Success Response (200) theme={null} { "status_code": 200, "message": "Provider configurations fetched successfully.", "data": [ { "id": 42, "provider": "twilio", "account_sid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "created_at": "2026-02-07T10:00:00Z" }, { "id": 43, "provider": "plivo", "account_sid": "MAyyyyyyyyyyyyyyyyyyyyyyy", "created_at": "2026-02-10T09:00:00Z" } ] } ``` ```json Error Response (401) theme={null} { "status_code": 401, "message": "Authentication credentials were not provided." } ``` # Get All Provider Configurations Retrieve all telephony provider configurations that have been set up for your organization. Each configuration represents a set of credentials (account SID) linked to a specific telephony provider. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Org-Handle | string | Yes | Organization domain handle | You can get the `Org-Handle` by hitting the [Get All Organizations](/api/space/organizations) API. The `domain_handle` field in the response is your Org-Handle. ### Response Fields | Field | Type | Description | | ------------ | ------- | --------------------------------------- | | status\_code | integer | HTTP status code | | message | string | Response message | | data | array | Array of provider configuration objects | ### Provider Config Object Fields | Field | Type | Description | | ------------ | ------- | ---------------------------------------- | | id | integer | Unique provider configuration identifier | | provider | string | Provider slug/identifier | | account\_sid | string | Account SID for the provider | | created\_at | string | Configuration creation timestamp | ## Common Error Codes | Status Code | Description | | ----------- | --------------------------------------------- | | 200 | Success - Configurations fetched successfully | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Invalid organization handle | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' }; // Get all provider configurations const getAllProviders = async () => { const response = await axios.get( 'https://unpod.ai/api/v2/platform/telephony/providers-configurations/', { headers } ); console.log(`Total configurations: ${response.data.data.length}`); response.data.data.forEach(config => { console.log(`Config #${config.id}: ${config.provider} - ${config.account_sid}`); }); return response.data.data; }; // Example usage getAllProviders(); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' } def get_all_providers(): """Get all provider configurations""" url = 'https://unpod.ai/api/v2/platform/telephony/providers-configurations/' response = requests.get(url, headers=headers) data = response.json() print(f"Total configurations: {len(data['data'])}") for config in data['data']: print(f"Config #{config['id']}: {config['provider']} - {config['account_sid']}") return data['data'] # Example usage get_all_providers() ``` ```bash cURL theme={null} # Get all provider configurations curl -X GET "https://unpod.ai/api/v2/platform/telephony/providers-configurations/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" ``` ## Best Practices 1. **Configuration ID**: Note each configuration `id` - it is used when connecting a provider to a bridge 2. **Multiple Providers**: You can have configurations for multiple providers to support different regions 3. **Security**: `auth_token` is never returned in responses for security - only `account_sid` is shown 4. **Error Handling**: Always handle potential errors and edge cases 5. **Regular Auditing**: Periodically review and remove unused provider configurations # Get Provider by ID Source: https://docs.unpod.ai/api/provider/get-provider-by-id GET /api/v2/platform/telephony/providers-configurations/{id}/ Retrieve a specific telephony provider configuration by its ID ```json Success Response (200) theme={null} { "status_code": 200, "message": "Provider configuration fetched successfully.", "data": { "id": 42, "provider": "twilio", "account_sid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "created_at": "2026-02-07T10:00:00Z" } } ``` ```json Error Response (404) theme={null} { "status_code": 404, "message": "Provider configuration not found." } ``` # Get Provider Configuration by ID Retrieve a specific telephony provider configuration by its unique integer identifier. Use this to inspect the details of a particular provider setup. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. ### Path Parameters | Name | Type | Required | Description | | ---- | ------- | -------- | ----------------------------------------------- | | id | integer | Yes | Unique identifier of the provider configuration | You can get the provider configuration `id` by hitting the [Get All Providers](/api/provider/get-all-providers) API. ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Org-Handle | string | Yes | Organization domain handle | You can get the `Org-Handle` by hitting the [Get All Organizations](/api/space/organizations) API. The `domain_handle` field in the response is your Org-Handle. ### Response Fields | Field | Type | Description | | ------------ | ------- | ------------------------------ | | status\_code | integer | HTTP status code | | message | string | Response message | | data | object | Provider configuration details | ### Provider Config Object Fields | Field | Type | Description | | ------------ | ------- | ---------------------------------------- | | id | integer | Unique provider configuration identifier | | provider | string | Provider slug/identifier | | account\_sid | string | Account SID for the provider | | created\_at | string | Configuration creation timestamp | ## Common Error Codes | Status Code | Description | | ----------- | -------------------------------------------- | | 200 | Success - Configuration fetched successfully | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Invalid organization handle | | 404 | Not Found - Provider configuration not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' }; // Get provider configuration by ID const getProviderById = async (id) => { const response = await axios.get( `https://unpod.ai/api/v2/platform/telephony/providers-configurations/${id}/`, { headers } ); const config = response.data.data; console.log(`Provider: ${config.provider}`); console.log(`Account SID: ${config.account_sid}`); return config; }; // Example usage getProviderById(42); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' } def get_provider_by_id(config_id: int): """Get a specific provider configuration by ID""" url = f'https://unpod.ai/api/v2/platform/telephony/providers-configurations/{config_id}/' response = requests.get(url, headers=headers) data = response.json() config = data['data'] print(f"Provider: {config['provider']}") print(f"Account SID: {config['account_sid']}") return config # Example usage get_provider_by_id(42) ``` ```bash cURL theme={null} # Get provider configuration by ID curl -X GET "https://unpod.ai/api/v2/platform/telephony/providers-configurations/42/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" ``` ## Best Practices 1. **ID Lookup**: Use [Get All Providers](/api/provider/get-all-providers) to find IDs before fetching individual records 2. **Error Handling**: Handle 404 responses when a configuration ID is no longer valid 3. **Security**: `auth_token` is never returned - only `account_sid` is visible for security 4. **Org-Handle**: Ensure the correct organization handle is used to access configurations within your org 5. **Validation**: Use this endpoint to verify a provider configuration exists before connecting it to a bridge # Get Telephony Providers Source: https://docs.unpod.ai/api/provider/get-telephony-providers GET /api/v2/platform/telephony/providers/ Retrieve a list of all available telephony providers in the system ```json Success Response (200) theme={null} { "status_code": 200, "message": "Voice infra providers fetched successfully.", "data": [ { "id": 1, "name": "Twilio", "slug": "twilio" }, { "id": 2, "name": "Plivo", "slug": "plivo" } ] } ``` ```json Error Response (401) theme={null} { "status_code": 401, "message": "Authentication credentials were not provided." } ``` # Get Telephony Providers Retrieve a list of all available telephony providers in the system. These are the supported providers that you can configure credentials for using the Provider Configurations endpoints. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. *** ## Get Telephony Providers Retrieve all supported telephony providers available for configuration. ```http theme={null} GET /api/v2/platform/telephony/providers/ ``` ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Org-Handle | string | Yes | Organization domain handle | You can get the `Org-Handle` by hitting the [Get All Organizations](/api/space/organizations) API. The `domain_handle` field in the response is your Org-Handle. ### Response Fields | Field | Type | Description | | ------------ | ------- | ------------------------- | | status\_code | integer | HTTP status code | | message | string | Response message | | data | array | Array of provider objects | ### Provider Object Fields | Field | Type | Description | | ----- | ------- | --------------------------------------------------- | | id | integer | Provider unique identifier | | name | string | Provider display name | | slug | string | URL-friendly provider identifier used in config API | ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------- | | 200 | Success - Providers fetched successfully | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Invalid organization handle | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' }; // Get all available telephony providers const getTelephonyProviders = async () => { const response = await axios.get( 'https://unpod.ai/api/v2/platform/telephony/providers/', { headers } ); console.log(`Available providers: ${response.data.data.length}`); response.data.data.forEach(provider => { console.log(`${provider.name} (id: ${provider.id}, slug: ${provider.slug})`); }); return response.data.data; }; // Example usage getTelephonyProviders(); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' } def get_telephony_providers(): """Get all available telephony providers""" url = 'https://unpod.ai/api/v2/platform/telephony/providers/' response = requests.get(url, headers=headers) data = response.json() print(f"Available providers: {len(data['data'])}") for provider in data['data']: print(f"{provider['name']} (id: {provider['id']}, slug: {provider['slug']})") return data['data'] # Example usage get_telephony_providers() ``` ```bash cURL theme={null} # Get all telephony providers curl -X GET "https://unpod.ai/api/v2/platform/telephony/providers/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" ``` ## Best Practices 1. **Provider ID**: Use the `id` field when creating provider configurations via the [Create Provider](/api/provider/create-provider) endpoint 2. **Provider Selection**: Choose the provider that best suits your region and call volume requirements 3. **Caching**: Cache the providers list locally since it changes infrequently 4. **Error Handling**: Always handle potential errors and implement retry logic 5. **Security**: Keep API tokens secure and rotate them regularly # Overview Source: https://docs.unpod.ai/api/provider/overview Introduction to the Providers API # Providers API The Providers API allows you to manage telephony providers and their configurations in your Unpod platform. It covers both listing available telephony providers and full CRUD operations for provider configurations. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. ## What You Can Do | Endpoint | Method | Description | | ----------------------------------------------------------- | ------ | -------------------------------------- | | `/api/v2/platform/telephony/providers/` | GET | List all available telephony providers | | `/api/v2/platform/telephony/providers-configurations/` | POST | Create a new provider configuration | | `/api/v2/platform/telephony/providers-configurations/` | GET | Get all provider configurations | | `/api/v2/platform/telephony/providers-configurations/{id}/` | GET | Get a specific provider configuration | | `/api/v2/platform/telephony/providers-configurations/{id}/` | PATCH | Update a provider configuration | | `/api/v2/platform/telephony/providers-configurations/{id}/` | DELETE | Delete a provider configuration | ## Base URL ``` https://unpod.ai/api/v2/platform/ ``` ## Authentication ```http theme={null} Authorization: Token your-api-token Org-Handle: your-org-handle ``` ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------- | | 200 | Success - Request completed successfully | | 204 | No Content - Resource deleted successfully | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Access denied | | 404 | Not Found - Configuration not found | | 500 | Internal Server Error | # Update Provider Source: https://docs.unpod.ai/api/provider/update-provider PATCH /api/v2/platform/telephony/providers-configurations/{id}/ Partially update a telephony provider configuration ```json Success Response (200) theme={null} { "status_code": 200, "message": "Provider configuration updated successfully.", "data": { "id": 42, "provider": "twilio", "account_sid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "created_at": "2026-02-07T10:00:00Z" } } ``` ```json Error Response (404) theme={null} { "status_code": 404, "message": "Provider configuration not found." } ``` # Update Provider Configuration Partially update an existing telephony provider configuration. This is useful for rotating credentials such as the `auth_token` without deleting and recreating the entire configuration. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. ### Path Parameters | Name | Type | Required | Description | | ---- | ------- | -------- | ----------------------------------------------- | | id | integer | Yes | Unique identifier of the provider configuration | You can get the provider configuration `id` by hitting the [Get All Providers](/api/provider/get-all-providers) API. ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Org-Handle | string | Yes | Organization domain handle | | Content-Type | string | Yes | `application/json` | ### Request Body All fields are optional - include only the fields you wish to update. | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------- | | auth\_token | string | No | Updated Auth Token / Secret key | | account\_sid | string | No | Updated Account SID | ### Response Fields | Field | Type | Description | | ------------ | ------- | ------------------------------- | | status\_code | integer | HTTP status code | | message | string | Response message | | data | object | Updated provider config details | ### Provider Config Object Fields | Field | Type | Description | | ------------ | ------- | ---------------------------------------- | | id | integer | Unique provider configuration identifier | | provider | string | Provider slug/identifier | | account\_sid | string | Account SID for the provider | | created\_at | string | Configuration creation timestamp | ## Common Error Codes | Status Code | Description | | ----------- | -------------------------------------------- | | 200 | Success - Configuration updated successfully | | 400 | Bad Request - Invalid update parameters | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Invalid organization handle | | 404 | Not Found - Provider configuration not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle', 'Content-Type': 'application/json' }; // Update a provider configuration const updateProvider = async (id, updates) => { const response = await axios.patch( `https://unpod.ai/api/v2/platform/telephony/providers-configurations/${id}/`, updates, { headers } ); console.log(`Provider #${response.data.data.id} updated successfully`); return response.data.data; }; // Example: rotate auth_token updateProvider(42, { auth_token: 'your-new-auth-token-here' }); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle', 'Content-Type': 'application/json' } def update_provider(config_id: int, updates: dict): """Partially update a provider configuration""" url = f'https://unpod.ai/api/v2/platform/telephony/providers-configurations/{config_id}/' response = requests.patch(url, headers=headers, json=updates) data = response.json() print(f"Provider #{data['data']['id']} updated successfully") return data['data'] # Example: rotate auth_token update_provider(42, {'auth_token': 'your-new-auth-token-here'}) ``` ```bash cURL theme={null} # Update a provider configuration (rotate auth token) curl -X PATCH "https://unpod.ai/api/v2/platform/telephony/providers-configurations/42/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your-new-auth-token-here" }' ``` ## Best Practices 1. **Partial Updates**: Only include fields you want to change - PATCH supports partial updates 2. **Credential Rotation**: Regularly rotate `auth_token` values for security without disrupting bridges 3. **Security**: Never expose `auth_token` values in logs or responses 4. **Verify Before Update**: Use [Get Provider by ID](/api/provider/get-provider-by-id) to confirm the configuration exists before patching 5. **Error Handling**: Handle 404 and 400 errors to catch invalid IDs or credential formats # Get All Spaces Source: https://docs.unpod.ai/api/space/get-all-spaces GET /api/v2/platform/spaces/ Retrieve details of all created spaces in your organization ```json Success Response (200) theme={null} { "count": 207, "status_code": 200, "message": "Spaces fetched successfully", "data": [ { "id": "sp_001", "name": "Sales Outreach Q1", "token": "8KZAMRAHSXXXXXXMAYNASMJC", "agent_handle": "space-agent-8qmk42nslp91wrh3dz7btxc4", "created_at": "2026-01-10T08:00:00Z" } ] } ``` ```json Error Response (401) theme={null} { "status_code": 401, "message": "Authentication credentials were not provided." } ``` *** ## Get All Spaces Retrieve a list of all spaces within your organization. Spaces are containers that organize your tasks, runs, and data collections. ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Org-Handle | string | Yes | Organization domain handle | ### Response Fields | Field | Type | Description | | ------------ | ------- | ---------------------- | | count | integer | Total number of spaces | | status\_code | integer | HTTP status code | | message | string | Response message | | data | array | Array of space objects | ### Space Object Fields | Field | Type | Description | | ------------- | ------ | ------------------------------------------------- | | id | string | Unique space identifier | | name | string | Space display name | | token | string | Public token for API access (use as space\_token) | | agent\_handle | string | Handle of the agent assigned to the space | | created\_at | string | Space creation timestamp (ISO 8601) | *** ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------- | | 200 | Success - Request completed successfully | | 400 | Bad Request - Invalid parameters provided | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Access denied to the resource | | 404 | Not Found - Organization not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' }; // Get all spaces const getAllSpaces = async () => { const response = await axios.get( 'https://unpod.ai/api/v2/platform/spaces/', { headers } ); console.log(`Total spaces: ${response.data.count}`); response.data.data.forEach(space => { console.log(`${space.name} - Token: ${space.token}`); }); return response.data.data; }; // Example usage getAllSpaces(); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' } def get_all_spaces(): """Get all spaces in the organization""" url = 'https://unpod.ai/api/v2/platform/spaces/' response = requests.get(url, headers=headers) data = response.json() print(f"Total spaces: {data['count']}") for space in data['data']: print(f"{space['name']} - Token: {space['token']}") return data['data'] # Example usage get_all_spaces() ``` ```bash cURL theme={null} # Get all spaces curl -X GET "https://unpod.ai/api/v2/platform/spaces/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" ``` ## Best Practices 1. **Org-Handle**: Always include the correct organization handle in your requests 2. **Space Token**: Note the `token` field - it is used as the `space_token` path parameter in other endpoints 3. **Agent Handle**: Use the `agent_handle` when creating tasks to assign them to the correct agent 4. **Error Handling**: Always handle potential errors and edge cases 5. **Caching**: Consider caching space data to reduce API calls since spaces change infrequently # Get Space by Token Source: https://docs.unpod.ai/api/space/get-space-by-token GET /api/v2/platform/spaces/{space_token}/ Retrieve configuration and metadata of a specific space by its token ```json Success Response (200) theme={null} { "status_code": 200, "message": "Space fetched successfully", "data": { "id": "sp_001", "name": "Sales Outreach Q1", "token": "8KZRTQP7BNW5XEDLORYUHMJC", "agent_handle": "space-agent-8qmk42nslp91wrh3dz7btxc4", "created_at": "2026-01-10T08:00:00Z" } } ``` ```json Error Response (404) theme={null} { "status_code": 404, "message": "Space not found." } ``` ```json Error Response (401) theme={null} { "status_code": 401, "message": "Authentication credentials were not provided." } ``` # Get Space by Token Retrieve configuration and metadata of a specific space identified by its token. This allows you to view a space's current setup, properties, and organization context. *** ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Org-Handle | string | Yes | Organization domain handle | ### Response Fields | Field | Type | Description | | ------------ | ------- | -------------------- | | status\_code | integer | HTTP status code | | message | string | Response message | | data | object | Space object details | ### Space Object Fields | Field | Type | Description | | ------------- | ------ | ------------------------------------------ | | id | string | Unique space identifier | | name | string | Space display name | | token | string | Public token for API access | | agent\_handle | string | Handle of the agent assigned to this space | | created\_at | string | Space creation timestamp (ISO 8601) | ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------- | | 200 | Success - Request completed successfully | | 400 | Bad Request - Invalid parameters provided | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Access denied to the resource | | 404 | Not Found - Space or Organization not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' }; // Get space by token const getSpaceByToken = async (spaceToken) => { const response = await axios.get( `https://unpod.ai/api/v2/platform/spaces/${spaceToken}/`, { headers } ); console.log(`Space name: ${response.data.data.name}`); console.log(`Agent handle: ${response.data.data.agent_handle}`); return response.data.data; }; // Example usage getSpaceByToken('8KZRTQP7BNW5XEDLORYUHMJC'); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' } def get_space_by_token(space_token: str): """Get a specific space by its token""" url = f'https://unpod.ai/api/v2/platform/spaces/{space_token}/' response = requests.get(url, headers=headers) data = response.json() print(f"Space name: {data['data']['name']}") print(f"Agent handle: {data['data']['agent_handle']}") return data['data'] # Example usage get_space_by_token('8KZRTQP7BNW5XEDLORYUHMJC') ``` ```bash cURL theme={null} # Get space by token curl -X GET "https://unpod.ai/api/v2/platform/spaces/8KZRTQP7BNW5XEDLORYUHMJC/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" ``` ## Best Practices 1. **Space Token**: Ensure you are using a valid space token from your organization 2. **Org-Handle**: Always include the correct organization handle in requests 3. **Error Handling**: Handle 404 responses when a space token may have been deleted or is invalid 4. **Security**: Keep API tokens secure and rotate them regularly 5. **Caching**: Consider caching space details to avoid repeated lookups for the same space # Get Tasks by Space Token Source: https://docs.unpod.ai/api/space/get-task-by-space-token GET /api/v2/platform/spaces/{space_token}/tasks/ Retrieve all tasks within a specific space using its token ```json Success Response (200) theme={null} { "count": 45, "status_code": 200, "message": "Tasks Fetched Successfully", "data": [ { "_id": "697debb84c27faa892bfa0cc", "thread_id": "thread_9xkL2mQpR7vNwY4sZ3cJ8hF1", "user_info": { "email": "user@example.com", "full_name": "John Doe" }, "task_id": "T8ff2ccdffe9a11f0878d43cd8a99e069", "run_id": "R8ff2ccdefe9a11f0878d43cd8a99e069", "task": { "objective": "Call the lead and discuss the project requirements." }, "input": { "name": "John Doe", "contact_number": "1234567890", "email": "john@example.com", "context": "Follow up on proposal" }, "output": { "call_id": "CALL_7dAb3kR9mXvQ2pLw", "start_time": "2026-02-07T05:57:45Z", "end_time": "2026-02-07T06:02:30Z", "duration": 285, "recording_url": "https://cdn.unpod.ai/recordings/CALL_7dAb3kR9mXvQ2pLw.mp3", "transcript": [ { "role": "agent", "content": "Hello, this is an AI assistant calling on behalf of Unpod. Am I speaking with John Doe?" }, { "role": "user", "content": "Yes, this is John." } ], "post_call_data": { "summary": "The agent successfully connected with John Doe and discussed project requirements.", "outcome": "interested", "sentiment": "positive" }, "call_type": "outbound", "call_status": "completed" }, "assignee": "space-agent-8qmk42nslp91wrh3dz7btxc4", "status": "completed", "execution_type": "call", "run_mode": "prod", "created": "2026-02-07T05:57:45Z", "modified": "2026-02-07T06:02:35Z" } ] } ``` ```json Error Response (206) theme={null} { "message": "Error fetching tasks", "errors": "Detailed error description" } ``` # Get Tasks by Space Token Fetch all tasks for a specific space using the space token. Supports pagination with `page` and `page_size` query parameters for efficient data retrieval. **Prerequisites:** Make sure you have your API Token ready. See [Authentication](/api/get-started/authentication) for details. *** ## Get Tasks by Space Token Retrieve all tasks associated with a specific space, with optional pagination. ```http theme={null} GET /api/v2/platform/spaces/{space_token}/tasks/ ``` ### Path Parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | -------------------------------------- | | space\_token | string | Yes | Public token identifying the workspace | You can get the `space_token` by hitting the [Get All Spaces](/api/space/get-all-spaces) API. The `token` field in the response is your Space Token. ### Query Parameters | Name | Type | Required | Description | | ---------- | ------- | -------- | ---------------------------------------- | | page | integer | No | Page number for pagination (default = 1) | | page\_size | integer | No | Number of tasks per page (default = 20) | ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Content-Type | string | Yes | `application/json` | ### Response Fields | Field | Type | Description | | ------------ | ------- | --------------------- | | count | integer | Total number of tasks | | status\_code | integer | HTTP status code | | message | string | Response message | | data | array | Array of task objects | ### Task Object Fields | Field | Type | Description | | --------------- | ------ | --------------------------------------------- | | \_id | string | Internal task document ID | | thread\_id | string | Thread identifier | | user\_info | object | User details (email, full\_name) | | task\_id | string | Unique task identifier | | run\_id | string | Parent run identifier | | task | object | Task definition with objective | | input | object | Input data for the task | | output | object | Task output with call details | | assignee | string | Agent handle assigned to the task | | status | string | Task status: `pending`, `completed`, `failed` | | execution\_type | string | Type of execution: `call`, `email`, etc. | | run\_mode | string | Execution mode: `dev`, `prod`, etc. | | created | string | Task creation timestamp | | modified | string | Last modified timestamp | ## Common Error Codes | Status Code | Description | | ----------- | ----------------------------------------------- | | 200 | Success - Data fetched successfully | | 206 | Partial Content - Business logic error occurred | | 400 | Bad Request - Invalid parameters provided | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Access denied to the resource | | 404 | Not Found - Space not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Content-Type': 'application/json' }; // Get tasks by space token with pagination const getTasksBySpace = async (spaceToken, page = 1, pageSize = 20) => { const response = await axios.get( `https://unpod.ai/api/v2/platform/spaces/${spaceToken}/tasks/`, { headers, params: { page, page_size: pageSize } } ); console.log(`Total tasks: ${response.data.count}`); response.data.data.forEach(task => { console.log(`Task ${task.task_id}: ${task.status}`); }); return response.data.data; }; // Example usage getTasksBySpace('your-space-token', 1, 20); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Content-Type': 'application/json' } def get_tasks_by_space(space_token: str, page: int = 1, page_size: int = 20): """Get tasks by space token with pagination""" url = f'https://unpod.ai/api/v2/platform/spaces/{space_token}/tasks/' params = {'page': page, 'page_size': page_size} response = requests.get(url, headers=headers, params=params) data = response.json() print(f"Total tasks: {data['count']}") for task in data['data']: print(f"Task {task['task_id']}: {task['status']}") return data['data'] # Example usage get_tasks_by_space('your-space-token', page=1, page_size=20) ``` ```bash cURL theme={null} # Get tasks by space token with pagination curl -X GET "https://unpod.ai/api/v2/platform/spaces/your-space-token/tasks/?page=1&page_size=20" \ -H "Authorization: Token your-api-token" \ -H "Content-Type: application/json" ``` ## Best Practices 1. **Pagination**: Use `page` and `page_size` parameters for efficient data retrieval in large datasets 2. **Space Token**: Ensure you are using a valid space token from your organization 3. **Error Handling**: Always handle potential errors and edge cases 4. **Status Filtering**: Filter tasks by status in your application logic to focus on relevant tasks 5. **Security**: Keep API tokens secure and rotate them regularly # Get all Organizations Source: https://docs.unpod.ai/api/space/organizations GET /api/v2/platform/organizations/ ```json Success Response (200) theme={null} { "count": 3, "status_code": 200, "message": "Organizations fetched successfully", "data": [ { "id": 1, "name": "Unpod TV", "domain_handle": "unpod.tv", "created_at": "2024-01-15T10:30:00Z" }, { "id": 2, "name": "Recalll", "domain_handle": "recalll.co", "created_at": "2024-03-20T08:00:00Z" } ] } ``` ```json Error Response (401) theme={null} { "status_code": 401, "message": "Authentication credentials were not provided." } ``` ## Organizations API Retrieve a list of all organizations associated with your API token. The `domain_handle` field in the response is used as the `Org-Handle` header in subsequent API requests. ### Response Fields | Field | Type | Description | | ------------ | ------- | ----------------------------- | | count | integer | Total number of organizations | | status\_code | integer | HTTP status code | | message | string | Response message | | data | array | Array of organization objects | ### Organization Object Fields | Field | Type | Description | | -------------- | ------- | -------------------------------------------------- | | id | integer | Unique organization identifier | | name | string | Organization display name | | domain\_handle | string | Domain handle used as `Org-Handle` in API requests | | created\_at | string | Organization creation timestamp (ISO 8601) | *** ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------- | | 200 | Success - Request completed successfully | | 400 | Bad Request - Invalid parameters provided | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Access denied to the resource | | 404 | Not Found - Organization not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' }; // Get all organizations const getOrganizations = async () => { const response = await axios.get( 'https://unpod.ai/api/v2/platform/organizations/', { headers } ); console.log(`Total organizations: ${response.data.count}`); response.data.data.forEach(org => { console.log(`${org.name} - Handle: ${org.domain_handle}`); }); return response.data.data; }; // Example usage getOrganizations(); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' } def get_organizations(): """Get all organizations""" url = 'https://unpod.ai/api/v2/platform/organizations/' response = requests.get(url, headers=headers) data = response.json() print(f"Total organizations: {data['count']}") for org in data['data']: print(f"{org['name']} - Handle: {org['domain_handle']}") return data['data'] # Example usage get_organizations() ``` ```bash cURL theme={null} # Get all organizations curl -X GET "https://unpod.ai/api/v2/platform/organizations/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" ``` ## Best Practices 1. **Org-Handle**: Use the `domain_handle` from the response as the `Org-Handle` header in all subsequent requests 2. **Caching**: Cache organization data locally to reduce API calls since organizations change infrequently 3. **Error Handling**: Always handle potential errors and edge cases 4. **Security**: Keep API tokens secure and rotate them regularly 5. **Multiple Orgs**: If you have access to multiple organizations, store all handles and switch between them as needed # Overview Source: https://docs.unpod.ai/api/space/overview Introduction to the Organisation API # Organisation API The Organisation API allows you to manage and retrieve information about organizations registered in your Unpod platform. **Prerequisites:** Make sure you have your API Token ready. See [Authentication](/api/get-started/authentication) for details. ## What You Can Do | Endpoint | Description | | ------------------------------------- | ------------------------------------- | | `GET /api/v2/platform/organizations/` | Retrieve all registered organizations | ## Base URL ``` https://unpod.ai/api/v2/platform/ ``` ## Authentication All Organisation API requests require a valid API token in the `Authorization` header: ```http theme={null} Authorization: Token your-api-token ``` ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------- | | 200 | Success - Request completed successfully | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Access denied to the resource | | 500 | Internal Server Error - Server-side error | # Overview Source: https://docs.unpod.ai/api/space/spaces-overview Introduction to the Spaces API # Spaces API The Spaces API allows you to manage and retrieve spaces (workspaces/groups) in your Unpod platform. Spaces are containers that organize your tasks, runs, and data collections. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. ## What You Can Do | Endpoint | Description | | -------------------------------------------------- | ------------------------------------------ | | `GET /api/v2/platform/spaces/` | Retrieve all spaces in your organization | | `GET /api/v2/platform/spaces/{space_token}/` | Retrieve a specific space by its token | | `GET /api/v2/platform/spaces/{space_token}/tasks/` | Retrieve all tasks within a specific space | ## Base URL ``` https://unpod.ai/api/v2/platform/ ``` ## Authentication All Spaces API requests require a valid API token and Org-Handle in the headers: ```http theme={null} Authorization: Token your-api-token Org-Handle: your-org-handle ``` You can get the `Org-Handle` by hitting the [Get All Organizations](/api/space/organizations) API. The `domain_handle` field in the response is your Org-Handle. ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------- | | 200 | Success - Request completed successfully | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Access denied to the resource | | 404 | Not Found - Space or Organization not found | | 500 | Internal Server Error - Server-side error | # Overview Source: https://docs.unpod.ai/api/telephony/bridges-overview Introduction to the Bridges API # Bridges API The Bridges API allows you to manage telephony bridges for connecting call participants. Bridges handle call routing, conferencing, and telephony channel management in your Unpod platform. **Prerequisites:** Make sure you have your API Token, Org-Handle, and Product-ID ready. See [Authentication](/api/get-started/authentication) for details. ## What You Can Do | Endpoint | Method | Description | | ---------------------------------------------------------------- | ------ | ------------------------------- | | `/api/v2/platform/telephony/bridges/` | POST | Create a new bridge | | `/api/v2/platform/telephony/bridges/` | GET | List all bridges | | `/api/v2/platform/telephony/bridges/{slug}/` | GET | Get bridge by slug | | `/api/v2/platform/telephony/bridges/{slug}/` | PATCH | Update a bridge | | `/api/v2/platform/telephony/bridges/{slug}/` | DELETE | Delete a bridge | | `/api/v2/platform/telephony/bridges/{slug}/connect-provider/` | POST | Connect provider to bridge | | `/api/v2/platform/telephony/numbers/` | GET | List telephony numbers | | `/api/v2/platform/telephony/bridges/{slug}/disconnect-provider/` | POST | Disconnect provider from bridge | ## Base URL ``` https://unpod.ai/api/v2/platform/ ``` ## Authentication ```http theme={null} Authorization: Token your-api-token Org-Handle: your-org-handle Product-ID: your-product-id ``` ## Bridge Lifecycle Animated bridge lifecycle diagram showing bridge creation in draft, provider connection, number assignment, active state, and deletion. ## Common Error Codes | Status Code | Description | | ----------- | --------------------------------------- | | 200 | Success | | 201 | Created successfully | | 400 | Bad Request - Invalid parameters | | 401 | Unauthorized - Invalid or missing token | | 404 | Not Found - Bridge not found | | 500 | Internal Server Error | # Connect Provider to Bridge Source: https://docs.unpod.ai/api/telephony/connect-provider-to-bridge POST /api/v2/platform/telephony/bridges/{slug}/connect-provider/ Connect a provider credential to a telephony bridge to add a number ```json Success Response (200) theme={null} { "status_code": 200, "message": "Provider connected to bridge successfully.", "data": { "message": "Provider connected successfully", "bridge_slug": "sales-bridge-001", "phone_number": "+1234567890" } } ``` ```json Error Response (400) theme={null} { "status_code": 400, "message": "Failed to connect provider.", "errors": "Provider configuration not found or phone number invalid" } ``` # Connect Provider to Bridge Connect a telephony provider configuration to a specific bridge, linking a phone number to the bridge for call routing. This enables inbound and outbound calls through the connected provider. **Prerequisites:** Make sure you have your API Token, Org-Handle, a configured Bridge, and a Provider Configuration ready. See [Authentication](/api/get-started/authentication) for details. The `provider_config_id` must be a valid provider configuration already created for your organization. You can only use a `phone_number` that is linked to the provider account. ### Path Parameters | Name | Type | Required | Description | | ---- | ------ | -------- | ------------------ | | slug | string | Yes | Unique bridge slug | You can get the bridge `slug` by hitting the [Get All Bridges](/api/telephony/get-all-bridges) API. The `slug` field in the response is your Bridge Slug. ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Org-Handle | string | Yes | Organization domain handle | | Content-Type | string | Yes | `application/json` | You can get the `Org-Handle` by hitting the [Get All Organizations](/api/space/organizations) API. The `domain_handle` field in the response is your Org-Handle. ### Request Body | Field | Type | Required | Description | | -------------------- | ------- | -------- | --------------------------------------------------- | | provider\_config\_id | integer | Yes | ID of the provider configuration to connect | | phone\_number | string | Yes | Phone number (E.164 format) to assign to the bridge | ### Response Fields | Field | Type | Description | | ------------ | ------- | ----------------- | | status\_code | integer | HTTP status code | | message | string | Response message | | data | object | Connection result | ### Data Object Fields | Field | Type | Description | | ------------- | ------ | ----------------------------------------- | | message | string | Success message | | bridge\_slug | string | The bridge slug the provider connected to | | phone\_number | string | The phone number assigned to the bridge | ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------------- | | 200 | Success - Provider connected successfully | | 400 | Bad Request - Invalid provider ID or phone number | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Invalid organization handle | | 404 | Not Found - Bridge not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle', 'Content-Type': 'application/json' }; // Connect a provider to a bridge const connectProviderToBridge = async (slug, providerConfigId, phoneNumber) => { const response = await axios.post( `https://unpod.ai/api/v2/platform/telephony/bridges/${slug}/connect-provider/`, { provider_config_id: providerConfigId, phone_number: phoneNumber }, { headers } ); console.log(`Provider connected to bridge: ${response.data.data.bridge_slug}`); console.log(`Phone number assigned: ${response.data.data.phone_number}`); return response.data.data; }; // Example usage connectProviderToBridge('sales-bridge-001', 42, '+1234567890'); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle', 'Content-Type': 'application/json' } def connect_provider_to_bridge(slug: str, provider_config_id: int, phone_number: str): """Connect a provider configuration to a bridge""" url = f'https://unpod.ai/api/v2/platform/telephony/bridges/{slug}/connect-provider/' payload = { 'provider_config_id': provider_config_id, 'phone_number': phone_number } response = requests.post(url, headers=headers, json=payload) data = response.json() print(f"Provider connected to bridge: {data['data']['bridge_slug']}") print(f"Phone number assigned: {data['data']['phone_number']}") return data['data'] # Example usage connect_provider_to_bridge('sales-bridge-001', 42, '+1234567890') ``` ```bash cURL theme={null} # Connect a provider to a bridge curl -X POST "https://unpod.ai/api/v2/platform/telephony/bridges/sales-bridge-001/connect-provider/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" \ -H "Content-Type: application/json" \ -d '{ "provider_config_id": 42, "phone_number": "+1234567890" }' ``` ## Best Practices 1. **Provider Config ID**: Get the correct `provider_config_id` from [Get All Providers](/api/provider/get-all-providers) before connecting 2. **Phone Number Format**: Use E.164 format (e.g., `+1234567890`) for phone numbers 3. **Verify Bridge**: Confirm the bridge exists using [Get Bridge by Slug](/api/telephony/get-bridge-by-slug) before connecting 4. **One Provider per Bridge**: A bridge typically connects to one provider - verify current state before re-connecting 5. **Error Handling**: Handle 400 errors that may indicate an invalid phone number or provider config 6. **Security**: Keep API tokens secure and rotate them regularly # Create Bridge Source: https://docs.unpod.ai/api/telephony/create-bridge POST /api/v2/platform/telephony/bridges/ Create a new telephony bridge for call routing ```json Success Response (201) theme={null} { "status_code": 201, "message": "Bridge created successfully.", "data": { "id": 314, "name": "Sales Bridge", "slug": "sales-bridge-001", "created_at": "2026-02-07T10:00:00Z" } } ``` ```json Error Response (400) theme={null} { "status_code": 400, "message": "Bridge creation failed.", "errors": "Slug already exists or name is required" } ``` # Create Bridge Create a new telephony bridge for call routing. Bridges are the core routing entities that link telephony providers and phone numbers to your Voice AI agents. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. *** ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Org-Handle | string | Yes | Organization domain handle | | Content-Type | string | Yes | `application/json` | You can get the `Org-Handle` by hitting the [Get All Organizations](/api/space/organizations) API. The `domain_handle` field in the response is your Org-Handle. ### Request Body | Field | Type | Required | Description | | ----- | ------ | -------- | --------------------------------------------- | | name | string | Yes | Display name for the bridge | | slug | string | Yes | URL-friendly unique identifier for the bridge | ### Response Fields | Field | Type | Description | | ------------ | ------- | ---------------------- | | status\_code | integer | HTTP status code | | message | string | Response message | | data | object | Created bridge details | ### Bridge Object Fields | Field | Type | Description | | ----------- | ------- | ------------------------------------ | | id | integer | Bridge unique identifier | | name | string | Bridge display name | | slug | string | Unique bridge slug identifier | | created\_at | string | Bridge creation timestamp (ISO 8601) | *** ## Common Error Codes | Status Code | Description | | ----------- | ----------------------------------------------- | | 201 | Created - Bridge created successfully | | 400 | Bad Request - Invalid or duplicate slug/name | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Invalid organization handle | | 409 | Conflict - Bridge with this slug already exists | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle', 'Content-Type': 'application/json' }; // Create a new telephony bridge const createBridge = async (name, slug) => { const response = await axios.post( 'https://unpod.ai/api/v2/platform/telephony/bridges/', { name, slug }, { headers } ); console.log(`Bridge created: ${response.data.data.slug} (ID: ${response.data.data.id})`); return response.data.data; }; // Example usage createBridge('Sales Bridge', 'sales-bridge-001'); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle', 'Content-Type': 'application/json' } def create_bridge(name: str, slug: str): """Create a new telephony bridge""" url = 'https://unpod.ai/api/v2/platform/telephony/bridges/' payload = {'name': name, 'slug': slug} response = requests.post(url, headers=headers, json=payload) data = response.json() print(f"Bridge created: {data['data']['slug']} (ID: {data['data']['id']})") return data['data'] # Example usage create_bridge('Sales Bridge', 'sales-bridge-001') ``` ```bash cURL theme={null} # Create a new telephony bridge curl -X POST "https://unpod.ai/api/v2/platform/telephony/bridges/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" \ -H "Content-Type: application/json" \ -d '{ "name": "Sales Bridge", "slug": "sales-bridge-001" }' ``` ## Best Practices 1. **Slug Naming**: Use descriptive, lowercase slugs (e.g., `sales-outbound-us`) to easily identify bridges 2. **Unique Slugs**: Ensure slugs are unique across your organization to avoid conflicts 3. **Post-Creation**: After creating a bridge, use [Connect Provider to Bridge](/api/telephony/connect-provider-to-bridge) to link a telephony provider 4. **Bridge ID**: Store the returned `id` and `slug` for subsequent bridge management operations 5. **Error Handling**: Handle 409 Conflict responses for duplicate slugs 6. **Security**: Keep API tokens secure and rotate them regularly # Delete Bridge Source: https://docs.unpod.ai/api/telephony/delete-bridge DELETE /api/v2/platform/telephony/bridges/{slug}/ Permanently delete a telephony bridge ```json Success Response (204) theme={null} HTTP/1.1 204 No Content ``` ```json Error Response (404) theme={null} { "status_code": 404, "message": "Bridge not found.", "errors": "No bridge exists with the provided slug" } ``` # Delete Bridge Permanently delete a telephony bridge by its slug. Once deleted, the bridge and all associated configurations are removed. This action cannot be undone. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. Deleting a bridge that has active phone numbers or is actively routing calls will disrupt service. Disconnect all providers and reassign numbers before deleting. ### Path Parameters | Name | Type | Required | Description | | ---- | ------ | -------- | ------------------ | | slug | string | Yes | Unique bridge slug | You can get the bridge `slug` by hitting the [Get All Bridges](/api/telephony/get-all-bridges) API. The `slug` field in the response is your Bridge Slug. ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Org-Handle | string | Yes | Organization domain handle | ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------- | | 204 | No Content - Bridge deleted successfully | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Invalid organization handle | | 404 | Not Found - Bridge not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' }; // Delete a bridge const deleteBridge = async (slug) => { await axios.delete( `https://unpod.ai/api/v2/platform/telephony/bridges/${slug}/`, { headers } ); console.log(`Bridge '${slug}' deleted successfully`); }; // Example usage deleteBridge('sales-bridge-001'); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' } def delete_bridge(slug: str): """Delete a telephony bridge""" url = f'https://unpod.ai/api/v2/platform/telephony/bridges/{slug}/' response = requests.delete(url, headers=headers) if response.status_code == 204: print(f"Bridge '{slug}' deleted successfully") else: print(f"Error: {response.json()}") # Example usage delete_bridge('sales-bridge-001') ``` ```bash cURL theme={null} # Delete a bridge curl -X DELETE "https://unpod.ai/api/v2/platform/telephony/bridges/sales-bridge-001/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" ``` ## Best Practices 1. **Disconnect First**: Always [disconnect the provider from the bridge](/api/telephony/disconnect-provider-from-bridge) before deleting it 2. **Irreversible**: Deletion is permanent - verify the correct `slug` before proceeding 3. **204 Response**: A successful delete returns HTTP 204 with no body - handle this in your code 4. **Error Handling**: Handle 404 gracefully - the bridge may have already been deleted 5. **Pre-check**: Use [Get Bridge by Slug](/api/telephony/get-bridge-by-slug) to verify the bridge exists and check for active numbers before deletion 6. **Security**: Keep API tokens secure and rotate them regularly # Disconnect Provider from Bridge Source: https://docs.unpod.ai/api/telephony/disconnect-provider-from-bridge POST /api/v2/platform/telephony/bridges/{slug}/disconnect-provider/ Disconnect a provider credential from a telephony bridge to remove the number ```json Success Response (200) theme={null} { "status_code": 200, "message": "Provider disconnected from bridge successfully.", "data": { "message": "Provider disconnected successfully", "bridge_slug": "sales-bridge-001" } } ``` ```json Error Response (400) theme={null} { "status_code": 400, "message": "Failed to disconnect provider.", "errors": "Phone number is not connected to this bridge" } ``` # Disconnect Provider from Bridge Disconnect a telephony provider from a specific bridge, removing the associated phone number from the bridge. Use this before deleting a bridge or reassigning a phone number to a different bridge. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. ### Path Parameters | Name | Type | Required | Description | | ---- | ------ | -------- | ------------------ | | slug | string | Yes | Unique bridge slug | You can get the bridge `slug` by hitting the [Get All Bridges](/api/telephony/get-all-bridges) API. The `slug` field in the response is your Bridge Slug. ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Org-Handle | string | Yes | Organization domain handle | | Content-Type | string | Yes | `application/json` | You can get the `Org-Handle` by hitting the [Get All Organizations](/api/space/organizations) API. The `domain_handle` field in the response is your Org-Handle. ### Request Body | Field | Type | Required | Description | | ------------- | ------ | -------- | ----------------------------------------------------- | | phone\_number | string | Yes | Phone number (E.164 format) to remove from the bridge | ### Response Fields | Field | Type | Description | | ------------ | ------- | -------------------- | | status\_code | integer | HTTP status code | | message | string | Response message | | data | object | Disconnection result | ### Data Object Fields | Field | Type | Description | | ------------ | ------ | ---------------------------------------------- | | message | string | Success message | | bridge\_slug | string | The bridge slug the provider disconnected from | ## Common Error Codes | Status Code | Description | | ----------- | --------------------------------------------------- | | 200 | Success - Provider disconnected successfully | | 400 | Bad Request - Phone number not connected or invalid | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Invalid organization handle | | 404 | Not Found - Bridge not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle', 'Content-Type': 'application/json' }; // Disconnect a provider from a bridge const disconnectProviderFromBridge = async (slug, phoneNumber) => { const response = await axios.post( `https://unpod.ai/api/v2/platform/telephony/bridges/${slug}/disconnect-provider/`, { phone_number: phoneNumber }, { headers } ); console.log(`Provider disconnected from bridge: ${response.data.data.bridge_slug}`); return response.data.data; }; // Example usage disconnectProviderFromBridge('sales-bridge-001', '+1234567890'); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle', 'Content-Type': 'application/json' } def disconnect_provider_from_bridge(slug: str, phone_number: str): """Disconnect a provider from a bridge""" url = f'https://unpod.ai/api/v2/platform/telephony/bridges/{slug}/disconnect-provider/' payload = {'phone_number': phone_number} response = requests.post(url, headers=headers, json=payload) data = response.json() print(f"Provider disconnected from bridge: {data['data']['bridge_slug']}") return data['data'] # Example usage disconnect_provider_from_bridge('sales-bridge-001', '+1234567890') ``` ```bash cURL theme={null} # Disconnect a provider from a bridge curl -X POST "https://unpod.ai/api/v2/platform/telephony/bridges/sales-bridge-001/disconnect-provider/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" \ -H "Content-Type: application/json" \ -d '{ "phone_number": "+1234567890" }' ``` ## Best Practices 1. **Pre-disconnect Check**: Verify the phone number is currently connected using [Get Bridge by Slug](/api/telephony/get-bridge-by-slug) before disconnecting 2. **Phone Number Format**: Use E.164 format (e.g., `+1234567890`) for the `phone_number` field 3. **Before Deletion**: Always disconnect providers before deleting a bridge to ensure clean teardown 4. **Error Handling**: Handle 400 errors - the phone number may not be connected to the specified bridge 5. **Service Disruption**: Disconnecting an active number will stop calls routing through the bridge immediately 6. **Security**: Keep API tokens secure and rotate them regularly # Get All Bridges Source: https://docs.unpod.ai/api/telephony/get-all-bridges GET /api/v2/platform/telephony/bridges/ Retrieve a list of all telephony bridges in the system ```json Success Response (200) theme={null} { "count": 6, "status_code": 200, "message": "Bridges fetched successfully.", "data": [ { "id": 314, "name": "Sales Bridge", "slug": "sales-bridge-001", "provider": "twilio", "numbers": [ { "id": 501, "number": "+1234567890", "status": "active" } ] } ] } ``` ```json Error Response (401) theme={null} { "status_code": 401, "message": "Authentication credentials were not provided." } ``` # Get All Bridges Retrieve a list of all telephony bridges in your organization, including their IDs, names, slugs, associated providers, and linked phone numbers for monitoring and call-routing management. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Org-Handle | string | Yes | Organization domain handle | You can get the `Org-Handle` by hitting the [Get All Organizations](/api/space/organizations) API. The `domain_handle` field in the response is your Org-Handle. ### Response Fields | Field | Type | Description | | ------------ | ------- | ----------------------- | | count | integer | Total number of bridges | | status\_code | integer | HTTP status code | | message | string | Response message | | data | array | Array of bridge objects | ### Bridge Object Fields | Field | Type | Description | | -------- | ------- | -------------------------------------------- | | id | integer | Bridge unique identifier | | name | string | Bridge display name | | slug | string | Unique bridge slug identifier | | provider | string | Connected provider slug (null if none) | | numbers | array | List of phone numbers assigned to the bridge | ### Number Object Fields | Field | Type | Description | | ------ | ------- | ----------------------------------- | | id | integer | Number assignment ID | | number | string | Phone number (E.164 format) | | status | string | Number status: `active`, `inactive` | ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------- | | 200 | Success - Bridges fetched successfully | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Invalid organization handle | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' }; // Get all bridges const getAllBridges = async () => { const response = await axios.get( 'https://unpod.ai/api/v2/platform/telephony/bridges/', { headers } ); console.log(`Total bridges: ${response.data.count}`); response.data.data.forEach(bridge => { console.log(`${bridge.name} (${bridge.slug}) - Numbers: ${bridge.numbers.length}`); }); return response.data.data; }; // Example usage getAllBridges(); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' } def get_all_bridges(): """Get all telephony bridges""" url = 'https://unpod.ai/api/v2/platform/telephony/bridges/' response = requests.get(url, headers=headers) data = response.json() print(f"Total bridges: {data['count']}") for bridge in data['data']: print(f"{bridge['name']} ({bridge['slug']}) - Numbers: {len(bridge['numbers'])}") return data['data'] # Example usage get_all_bridges() ``` ```bash cURL theme={null} # Get all telephony bridges curl -X GET "https://unpod.ai/api/v2/platform/telephony/bridges/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" ``` ## Best Practices 1. **Bridge Slug**: Note each bridge's `slug` - it is used as a path parameter in other bridge endpoints 2. **Provider Check**: Verify bridges have a connected provider before routing calls through them 3. **Number Assignment**: Bridges without numbers in the `numbers[]` array cannot receive inbound calls 4. **Error Handling**: Always handle potential errors and edge cases 5. **Security**: Keep API tokens secure and rotate them regularly # Get Bridge by Slug Source: https://docs.unpod.ai/api/telephony/get-bridge-by-slug GET /api/v2/platform/telephony/bridges/{slug}/ Retrieve details of a specific telephony bridge using its slug ```json Success Response (200) theme={null} { "status_code": 200, "message": "Bridge fetched successfully.", "data": { "id": 314, "name": "Sales Bridge", "slug": "sales-bridge-001", "numbers": [ { "id": 501, "number_id": 25, "number": "+1234567890", "state": "ASSIGNED", "active": true, "channels_count": 2, "status": "active" } ] } } ``` ```json Error Response (404) theme={null} { "status_code": 404, "message": "Bridge not found.", "errors": "No bridge exists with the provided slug" } ``` # Get Bridge by Slug Retrieve full details of a specific telephony bridge using its unique slug identifier, including its assigned phone numbers and current configuration. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. ### Path Parameters | Name | Type | Required | Description | | ---- | ------ | -------- | ------------------ | | slug | string | Yes | Unique bridge slug | You can get the bridge `slug` by hitting the [Get All Bridges](/api/telephony/get-all-bridges) API. The `slug` field in the response is your Bridge Slug. ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Org-Handle | string | Yes | Organization domain handle | You can get the `Org-Handle` by hitting the [Get All Organizations](/api/space/organizations) API. The `domain_handle` field in the response is your Org-Handle. ### Response Fields | Field | Type | Description | | ------------ | ------- | ---------------- | | status\_code | integer | HTTP status code | | message | string | Response message | | data | object | Bridge details | ### Bridge Object Fields | Field | Type | Description | | ------- | ------- | ------------------------------ | | id | integer | Bridge unique identifier | | name | string | Bridge display name | | slug | string | Unique bridge slug identifier | | numbers | array | List of assigned phone numbers | ### Number Object Fields | Field | Type | Description | | --------------- | ------- | -------------------------------------- | | id | integer | Number assignment ID | | number\_id | integer | Phone number ID | | number | string | Phone number (E.164 format) | | state | string | Number state: `ASSIGNED`, `UNASSIGNED` | | active | boolean | Whether number is active | | channels\_count | integer | Number of active channels | | status | string | Number status: `active`, `inactive` | ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------- | | 200 | Success - Bridge fetched successfully | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Invalid organization handle | | 404 | Not Found - Bridge not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' }; // Get bridge by slug const getBridgeBySlug = async (slug) => { const response = await axios.get( `https://unpod.ai/api/v2/platform/telephony/bridges/${slug}/`, { headers } ); const bridge = response.data.data; console.log(`Bridge: ${bridge.name} (${bridge.slug})`); console.log(`Assigned numbers: ${bridge.numbers.length}`); return bridge; }; // Example usage getBridgeBySlug('sales-bridge-001'); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' } def get_bridge_by_slug(slug: str): """Get bridge details by slug""" url = f'https://unpod.ai/api/v2/platform/telephony/bridges/{slug}/' response = requests.get(url, headers=headers) data = response.json() bridge = data['data'] print(f"Bridge: {bridge['name']} ({bridge['slug']})") print(f"Assigned numbers: {len(bridge['numbers'])}") return bridge # Example usage get_bridge_by_slug('sales-bridge-001') ``` ```bash cURL theme={null} # Get bridge by slug curl -X GET "https://unpod.ai/api/v2/platform/telephony/bridges/sales-bridge-001/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" ``` ## Best Practices 1. **Slug Validation**: Handle 404 errors when a bridge slug may have been deleted or renamed 2. **Number Inspection**: Check the `numbers` array to verify active phone numbers before routing calls 3. **Active Status**: Only numbers with `active: true` and `status: active` are ready to handle calls 4. **Error Handling**: Always handle potential errors and edge cases 5. **Security**: Keep API tokens secure and rotate them regularly # Get Telephony Numbers Source: https://docs.unpod.ai/api/telephony/numbers GET /api/v2/platform/telephony/numbers/ Retrieve list of all telephony numbers ```json Success Response (200) — With Org-Handle theme={null} { "status_code": 200, "message": "Telephony numbers fetched successfully.", "data": [ { "id": 501, "number": "+15551234567", "state": "ASSIGNED", "active": true }, { "id": 502, "number": "+15559876543", "state": "NOT_ASSIGNED", "active": true } ] } ``` ```json Success Response (200) — Without Org-Handle theme={null} { "status_code": 200, "message": "Telephony numbers fetched successfully.", "data": [ { "id": 502, "number": "+15559876543", "state": "NOT_ASSIGNED", "active": true } ] } ``` ```json Error Response (401) theme={null} { "status_code": 401, "message": "Authentication credentials were not provided." } ``` # Get Telephony Numbers Retrieve a list of telephony numbers. Behavior depends on the `Org-Handle` header: * **With Org-Handle** — returns your organization's own numbers (any state) plus the shared unassigned pool (`NOT_ASSIGNED`). * **Without Org-Handle** — returns only the shared unassigned pool. **Prerequisites:** Make sure you have your API Token ready. See [Authentication](/api/get-started/authentication) for details. ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Org-Handle | string | No | Organization domain handle | You can get the `Org-Handle` by hitting the [Get All Organizations](/api/space/organizations) API. The `domain_handle` field in the response is your Org-Handle. ### Response Fields | Field | Type | Description | | ------------ | ------- | ----------------------- | | status\_code | integer | HTTP status code | | message | string | Response message | | data | array | Array of number objects | ### Number Object Fields | Field | Type | Description | | ------ | ------- | ------------------------------------------- | | id | integer | Unique number id (use this in attach calls) | | number | string | Phone number in E.164 format | | state | string | `NOT_ASSIGNED` or `ASSIGNED` | | active | boolean | Whether the number is usable | ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------- | | 200 | Success - Numbers fetched successfully | | 401 | Unauthorized - Invalid or missing API token | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' // optional — omit for pool numbers only }; // Get org numbers + pool numbers const getTelephonyNumbers = async () => { const response = await axios.get( 'https://unpod.ai/api/v2/platform/telephony/numbers/', { headers } ); const numbers = response.data.data; console.log(`Total numbers: ${numbers.length}`); numbers.forEach(num => { console.log(`${num.number} - ${num.state}`); }); return numbers; }; // Get only unassigned pool numbers (no Org-Handle) const getPoolNumbers = async () => { const response = await axios.get( 'https://unpod.ai/api/v2/platform/telephony/numbers/', { headers: { 'Authorization': headers.Authorization } } ); return response.data.data; }; // Example usage getTelephonyNumbers(); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle' # optional — omit for pool numbers only } def get_telephony_numbers(): """Get org numbers + pool numbers""" url = 'https://unpod.ai/api/v2/platform/telephony/numbers/' response = requests.get(url, headers=headers) data = response.json() numbers = data['data'] print(f"Total numbers: {len(numbers)}") for num in numbers: print(f"{num['number']} - {num['state']}") return numbers def get_pool_numbers(): """Get only unassigned pool numbers (no Org-Handle)""" pool_headers = {k: v for k, v in headers.items() if k != 'Org-Handle'} url = 'https://unpod.ai/api/v2/platform/telephony/numbers/' response = requests.get(url, headers=pool_headers) return response.json()['data'] # Example usage get_telephony_numbers() ``` ```bash cURL theme={null} # With Org-Handle — org numbers + pool numbers curl -X GET "https://unpod.ai/api/v2/platform/telephony/numbers/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" # Without Org-Handle — pool numbers only curl -X GET "https://unpod.ai/api/v2/platform/telephony/numbers/" \ -H "Authorization: Token your-api-token" ``` ## Best Practices 1. **Org Scoping**: Pass `Org-Handle` to see your org's numbers; omit it to see only pool numbers 2. **Number Assignment**: Use a number's `id` when attaching it to a [trunk](/telephony/trunks/attach-numbers) 3. **State Awareness**: Check `state` to know if a number is `NOT_ASSIGNED` (available) or `ASSIGNED` (in use) 4. **E.164 Format**: All numbers are returned in E.164 format — use this format consistently in all API calls 5. **Security**: Keep API tokens secure and rotate them regularly # Update Bridge Source: https://docs.unpod.ai/api/telephony/update-bridge PATCH /api/v2/platform/telephony/bridges/{slug}/ Update an existing telephony bridge configuration ```json Success Response (200) theme={null} { "status_code": 200, "message": "Bridge updated successfully.", "data": { "id": 314, "name": "Sales Bridge - Updated", "slug": "sales-bridge-001", "numbers": [ { "id": 501, "number": "+1234567890", "status": "active" } ] } } ``` ```json Error Response (404) theme={null} { "status_code": 404, "message": "Bridge not found." } ``` # Update Bridge Partially update an existing telephony bridge's configuration. Currently supports updating the bridge's display name. **Prerequisites:** Make sure you have your API Token and Org-Handle ready. See [Authentication](/api/get-started/authentication) for details. ### Path Parameters | Name | Type | Required | Description | | ---- | ------ | -------- | ------------------ | | slug | string | Yes | Unique bridge slug | You can get the bridge `slug` by hitting the [Get All Bridges](/api/telephony/get-all-bridges) API. The `slug` field in the response is your Bridge Slug. ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------- | | Authorization | string | Yes | API Key format: `Token ` | | Org-Handle | string | Yes | Organization domain handle | | Content-Type | string | Yes | `application/json` | ### Request Body All fields are optional - include only the fields you wish to update. | Field | Type | Required | Description | | ----- | ------ | -------- | ----------------------------------- | | name | string | No | Updated display name for the bridge | ### Response Fields | Field | Type | Description | | ------------ | ------- | ---------------------- | | status\_code | integer | HTTP status code | | message | string | Response message | | data | object | Updated bridge details | ### Bridge Object Fields | Field | Type | Description | | ------- | ------- | ------------------------------ | | id | integer | Bridge unique identifier | | name | string | Updated bridge display name | | slug | string | Unique bridge slug identifier | | numbers | array | List of assigned phone numbers | ## Common Error Codes | Status Code | Description | | ----------- | ------------------------------------------- | | 200 | Success - Bridge updated successfully | | 400 | Bad Request - Invalid update parameters | | 401 | Unauthorized - Invalid or missing API token | | 403 | Forbidden - Invalid organization handle | | 404 | Not Found - Bridge not found | | 500 | Internal Server Error - Server-side error | ## Code Examples ```javascript Node.js theme={null} const axios = require('axios'); const headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle', 'Content-Type': 'application/json' }; // Update a bridge const updateBridge = async (slug, updates) => { const response = await axios.patch( `https://unpod.ai/api/v2/platform/telephony/bridges/${slug}/`, updates, { headers } ); console.log(`Bridge updated: ${response.data.data.name}`); return response.data.data; }; // Example usage updateBridge('sales-bridge-001', { name: 'Sales Bridge - Updated' }); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Token your-api-token', 'Org-Handle': 'your-org-handle', 'Content-Type': 'application/json' } def update_bridge(slug: str, updates: dict): """Update an existing telephony bridge""" url = f'https://unpod.ai/api/v2/platform/telephony/bridges/{slug}/' response = requests.patch(url, headers=headers, json=updates) data = response.json() print(f"Bridge updated: {data['data']['name']}") return data['data'] # Example usage update_bridge('sales-bridge-001', {'name': 'Sales Bridge - Updated'}) ``` ```bash cURL theme={null} # Update a bridge name curl -X PATCH "https://unpod.ai/api/v2/platform/telephony/bridges/sales-bridge-001/" \ -H "Authorization: Token your-api-token" \ -H "Org-Handle: your-org-handle" \ -H "Content-Type: application/json" \ -d '{ "name": "Sales Bridge - Updated" }' ``` ## Best Practices 1. **Partial Updates**: Only include fields you want to change - PATCH supports partial updates 2. **Slug Immutable**: The `slug` cannot be changed after creation - use descriptive slugs from the start 3. **Verify Before Update**: Use [Get Bridge by Slug](/api/telephony/get-bridge-by-slug) to confirm the bridge exists before patching 4. **Error Handling**: Handle 404 errors when the bridge slug is invalid or the bridge has been deleted 5. **Security**: Keep API tokens secure and rotate them regularly # Chat Source: https://docs.unpod.ai/get-started/chat Your trained agent, over an OpenAI-compatible chat API. One URL swap. Like chat completions - except the model is your trained agent. A playbook + a small model trained on your use case, or any third-party model, served as one OpenAI-compatible endpoint. Text in, text out. ## How it connects | Your stack | You swap | Unpod runs | | ----------------------------------- | ------------------------------------- | ----------------------- | | LiveKit · Pipecat · Vapi · chat app | the LLM base URL (or the `llm=` slot) | agent, sessions, memory | ## Go live in 3 steps ### 1. Get an endpoint Build and publish a playbook in the [Playground](https://superdialog.unpod.ai/playground), then open **Deploy as Endpoint → Manage API Keys**. You get the endpoint, a model id (`public:PB_...`), and prefilled snippets. See [Publish & Share](/playbook/publish-and-share). ### 2. Point your stack at it ```bash theme={null} curl -X POST "https://inference.unpod.ai/v1/chat/completions" \ -H "Authorization: Bearer $UNPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"public:PB_7ZRMzCA1ojQ9LlcK","messages":[{"role":"user","content":"hi"}],"user":"sess_abc"}' ``` Any OpenAI-compatible SDK works - swap base URL, key, and model. `user` is your session id, so the conversation stays stateful across calls. ```python theme={null} import os from livekit.agents import AgentSession from livekit.plugins import openai # The playbook holds the conversation state server-side, so pin a stable id # per call -- without `user` every turn restarts the playbook at step 0. session = AgentSession( llm=openai.LLM( model="playbook-5", base_url="https://inference.unpod.ai/v1", api_key=os.environ["UNPOD_API_KEY"], user=ctx.room.name, ), stt=..., # your STT tts=..., # your TTS ) # No user message on the first turn -> the playbook speaks its own opener. await session.generate_reply() # HTTP 410 session_ended = the playbook hung up. Close the room; do not retry. ``` The playbook is a hosted endpoint in the `llm=` slot - no agent code in-process. Full guide: [LiveKit embedding](/superdialog/embedding-guides/livekit). ```python theme={null} import os from pipecat.pipeline.pipeline import Pipeline from pipecat.services.openai.llm import OpenAILLMService # The playbook holds the conversation state server-side. Pipecat has no `user` # passthrough, so pin the stable id as a header -- without it every turn # restarts the playbook at step 0. llm = OpenAILLMService( model="playbook-5", base_url="https://inference.unpod.ai/v1", api_key=os.environ["UNPOD_API_KEY"], default_headers={"X-Session-Id": session_id}, ) pipeline = Pipeline([transport.input(), stt, llm, tts, transport.output()]) # HTTP 410 session_ended = the playbook hung up. End the pipeline; do not retry. ``` Full guide: [Pipecat embedding](/superdialog/embedding-guides/pipecat). ### 3. Test it `curl` the endpoint, or talk to the same agent in the [Playground Preview](https://superdialog.unpod.ai/playground?tab=preview). Same brain, same behaviour. ## Go deeper Full request/response reference. Describe the job; the playbook writes itself. LiveKit, Pipecat, FastAPI, CLI. Persona, checkpoints, slots - the format. # Core Concepts Source: https://docs.unpod.ai/get-started/core-concepts The single source of truth for Unpod terminology - every term defined once, with deep links. This page defines every Unpod term once. Other pages link here instead of redefining things. If a definition seems to conflict elsewhere, this page wins. The one rule that explains the whole architecture: **the wire between Unpod and your code carries text, not audio.** Unpod owns the phone call, the speech, and the carriers. You own the brain that decides what to say. Everything below follows from that split. ## Use It Like a Model Train an agent on your use case, then use it like a model - a playbook + a small model trained on your workflows, or any third-party model, over whichever surface fits your stack: | Surface | I/O | Live in | | --------------------------------- | ------------------- | ----------- | | [Chat](/get-started/chat) | text in, text out | 1 URL swap | | [Realtime](/get-started/realtime) | audio in, audio out | 1 WebSocket | | [Phone](/get-started/phone) | a phone number | 1 click | Not sure which? [Getting Started](/get-started/getting-started). ## Architecture at a Glance A single inbound call, end to end: Animated Unpod voice stack diagram showing phone, browser, and mobile entrypoints flowing through the managed speech layer into your AgentRunner, dialog machine, and tools. Read it as: a **Caller** enters through a phone number, browser, or mobile app. For phone calls, the **Number** routes into Unpod's managed speech layer - over Unpod's own carrier capacity, or over a **Trunk** you registered if you brought your own number. Unpod transcribes the audio with the call's **voice profile** (STT) and sends plain text across the **Bridge** over a WebSocket to your **AgentRunner**. Your runner hands each turn to your **Agent** (your brain). Your reply text crosses back over the Bridge, Unpod synthesises it (TTS), and the caller hears it. SuperDialog is one option for the brain - powerful, but optional. You own everything from the AgentRunner down. Everything above it is managed. ## Glossary One canonical definition each. Headings are anchor-able, so other pages can deep link (for example `/get-started/core-concepts#pipe`). ### Number A phone number callers dial. Inbound calls arrive on a number and route to the **Speech Pipe** it is attached to. Numbers come from Unpod directly or you bring your own (BYON). You never configure a carrier. See [Numbers](/speech-stack/numbers). ### Trunk The SIP connection between your own carrier and Unpod - **only needed for BYON** (bringing a number you already own). Numbers provisioned from Unpod need no trunk and no carrier account. If you do bring your own, you register the trunk once with its SIP credentials and then work with numbers, not SIP. See [Trunks](/speech-stack/numbers#trunks). ### Voice Profile A bundle of STT + TTS provider configuration: which providers recognise and synthesise speech, which language and voice, latency, and failover order. Profiles are a read-only catalog you pick from - you reference one by name or `profile_id` when creating a Speech Pipe. See [Voice Profiles](/speech-stack/voice-profiles). ### Pipe A **Speech Pipe** is the configuration entity that binds a call together: a name, a voice profile, recording and duration settings, and the `agent_id` that points at your runner. Numbers attach to a pipe; outbound calls run through a pipe. The pipe is the anchor that connects a number, a voice profile, and your agent. See [Pipes](/speech-stack/pipes). ### Bridge The text-routing seam inside Unpod between the speech pipeline and your code. Transcribed caller text crosses the Bridge to your AgentRunner; your reply text crosses back to be synthesised. The Bridge is why your code never touches audio. It is Unpod-internal infrastructure - you do not configure it; you'll see the term in WebSocket frame names and in the legacy [Bridges API](/api/telephony/bridges-overview). ### Agent (Brain) Your conversation logic - whatever decides what to say next. It can be a SuperDialog `DialogMachine`, a LangChain chain, a plain HTTP endpoint, or custom Python. Unpod is brain-agnostic: it routes text in and text out. "Agent" and "brain" mean the same thing here. See [Bring Your Agent](/speech-stack/bring-your-agent). ### AgentRunner A long-lived Python process you run. It registers with the Unpod orchestrator over WebSocket, advertises capacity, and serves a per-call bridge that Unpod dials into. For each call it builds a `CallContext` and invokes your entrypoint. You identify it with an `agent_id` (see [IDs You'll Meet](#ids-youll-meet)). See [SDK Setup](/speech-stack/agent-runner). ### Session Your control interface for one live call, reached as `ctx.session`. It exposes controls (`say()`, `transfer_to_human()`, `end()`, recording controls), hooks, metrics, and the `dialog_machine` slot where you plug in your brain. Calling `session.run()` keeps the call alive and routes each transcribed turn to your brain. See [Session Controls](/speech-stack/agent-runner). ### CallContext The per-call metadata envelope your entrypoint receives: `async def entrypoint(ctx: CallContext)`. It carries `call_id`, `session_id`, `agent_id`, `direction` (`"inbound"` or `"outbound"`), `user_number`, any `instructions` and `data` from dispatch, and the live `session` you control the call through. It also exposes `runner_id` - the runner's own configured `agent_id`. On a multi-tenant runner the call's `agent_id` (the agent the call was dispatched to) and `runner_id` can differ; use `runner_id` when you need to know which pool this process registered under. ### Space A Platform concept, not an SDK one. A Space is a workspace container in the Unpod Platform that organises agents, tasks, runs, and data. The Platform's REST API addresses a space by its **space token**. You only meet spaces when you use the hosted Platform or its REST API - the voice SDK does not require one. ## Two APIs The `unpod` SDK package contains two distinct halves. Know which one you are using. | | Management API | Connectivity API | | ----------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------- | | Protocol | REST (HTTPS) | WebSocket (WSS) | | Entry point | `Client` / `AsyncClient` | `AgentRunner` / `Session` | | Purpose | Provision resources | Handle live calls | | You call it | Before calls | During calls | | Examples | `client.numbers`, `client.voice_profiles`, `client.pipes`, `client.trunks`, `client.calls` | `AgentRunner(...).start()`, `ctx.session.say(...)` | **Management API** is for setup and orchestration: register a trunk, sync numbers, create a Speech Pipe, trigger an outbound call, fetch transcripts. You construct a `Client` (sync) or `AsyncClient` (async); it reads `UNPOD_API_KEY` from the environment. ```python theme={null} from unpod import AsyncClient client = AsyncClient() # REST, reads UNPOD_API_KEY pipe = await client.pipes.create(name="support", agent_id="support-bot") ``` **Connectivity API** is for the call itself: your `AgentRunner` holds a persistent WSS connection to the orchestrator, and each call gives you a live `Session` to act on. ```python theme={null} from unpod import AgentRunner, CallContext async def entrypoint(ctx: CallContext) -> None: await ctx.session.say("Hello") # WSS, live call await ctx.session.run() AgentRunner(entrypoint=entrypoint, agent_id="support-bot").start() ``` ## IDs You'll Meet Four identifiers cause most first-run failures. They are not interchangeable. **`agent_id` and `pipe_id` are different things.** Mismatching them is the most common reason a call never reaches your runner. The `agent_id` you pass to `AgentRunner(...)` must exactly match the `agent_id` on the Speech Pipe. | ID | What it identifies | Where it comes from | | ------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`agent_id`** | Your runner pool - which AgentRunner should handle a call | A string **you choose**. Passed to both `AgentRunner(agent_id=...)` and `client.pipes.create(agent_id=...)`. They must match exactly. | | **`pipe_id`** | One Speech Pipe in the Speech Stack | A UUID **Unpod assigns** when you create the pipe. Used in REST calls (`numbers.attach`, `calls.create`). | | **Space token** | A Platform workspace | A token from the Platform's Spaces API. Used only in the hosted Platform and its REST API, not in the voice SDK. | | **Runner agent ID** | Same as `agent_id` | Just another name for `agent_id` as seen from the runner side. Internally the runner derives a `worker_id` (`#`) per process, but you never set that. | Wiring it correctly: ```python theme={null} # 1. Pipe says: route my calls to the "support-bot" agent. pipe = await client.pipes.create(name="Support", agent_id="support-bot") print(pipe.pipe_id) # UUID from Unpod -> use in REST calls # 2. Runner says: I am the "support-bot" agent. AgentRunner(entrypoint=entrypoint, agent_id="support-bot").start() # ^^^^^^^^^^^^ # must equal the pipe's agent_id, or calls never arrive ``` If a call rings but your runner never wakes up, check this match first. ## Naming: The Four Product Terms Unpod is one company with one platform, described at four altitudes. Use these terms precisely. | Term | What it means | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Unpod** | The company and the platform as a whole - everything below combined. | | **Speech Stack** | The voice infrastructure plus the `unpod` SDK: numbers, trunks, voice profiles, pipes, STT/TTS, and the AgentRunner runtime. This is what a Python dev builds against. | | **SuperDialog** | The optional dialog framework (`superdialog` package): flow graphs, tools, and state for structured conversations. One choice of brain - not required. | | **Platform** | The hosted UI and self-hostable stack: dashboard, agent studio, spaces, analytics, and telephony management on top of the Speech Stack. | ## Next Steps Wire a number, a pipe, and a runner end to end. Numbers, voice profiles, pipes, and the AgentRunner SDK. The optional framework for structured conversation flows. Act on a live call - say, transfer, end, record. # Make Your First Phone Call Source: https://docs.unpod.ai/get-started/first-phone-call Take the agent you built in the browser quickstart to a real inbound or outbound phone call. You built and talked to your agent in the browser in the [Quickstart](/get-started/quickstart). Now give it a phone number. Nothing about the agent changes - same `entrypoint`, same brain, same [`AgentRunner`](/get-started/core-concepts#agentrunner). The only new work is provisioning: telling Unpod which number routes to your agent. ## Prerequisites * The agent from the [Quickstart](/get-started/quickstart), running * **A number in your account.** Provision one from the dashboard under **Dev Platform → Numbers**. No carrier account, no SIP trunk - see [Numbers](/speech-stack/numbers). Already own a number elsewhere? You can bring it over a [trunk](/speech-stack/numbers#trunks) instead. That is the only path that needs carrier credentials. ## Step 1 - Provision the number Provisioning uses the **Management API** - the REST half of the [SDK](https://github.com/unpod-ai/unpod-python-sdk), reached through `AsyncClient`. It reads `UNPOD_API_KEY` and derives its REST endpoint from `UNPOD_BASE_URL` (`https:///platform`) - both set in the [Quickstart](/get-started/quickstart#step-2-set-two-keys). If you need one-off overrides in code, pass `base_url=` to `AsyncClient` or `AgentRunner`; those arguments win over `.env` for that process only. Run this once to pick a voice profile, create the pipe, and attach a free number from your account: ```python theme={null} # setup.py - run once to provision your phone number import asyncio from unpod import AsyncClient async def setup() -> None: async with AsyncClient() as client: # 1. Pick a voice profile from the read-only catalog. profiles = await client.voice_profiles.list(language="en") if not profiles: print("No voice profiles found.") return vp = profiles[0] print(f"Using voice profile: {vp.name} ({vp.profile_id})") # 2. Create the Speech Pipe. agent_id MUST match your AgentRunner. pipe = await client.pipes.create( name="browser-agent", voice_profile=vp.name, # name (case-insensitive) or profile_id agent_id="browser-playground", # must match AgentRunner's agent_id recording=True, max_call_duration_s=600, ) print(f"Created Speech Pipe: {pipe.pipe_id}") # 3. Find a number in your account that is not attached yet. numbers = await client.numbers.list(status="active") free = [n for n in numbers if n.pipe_id is None] if not free: print("No free numbers - provision one under Dev Platform -> Numbers.") return # 4. Attach it to the pipe. number = await client.numbers.attach(number_id=free[0].number_id, pipe_id=pipe.pipe_id) print(f"Attached {number.number} -> Speech Pipe {pipe.pipe_id}") asyncio.run(setup()) ``` What it does: 1. **`voice_profiles.list()`** - pick a voice from the read-only catalog. 2. **`pipes.create()`** - bind that voice to your `agent_id`. 3. **`numbers.list()`** - find a number in your account with no pipe attached. 4. **`numbers.attach()`** - route that number to the pipe. If step 3 finds nothing, provision a number from the dashboard first. BYON numbers arrive over a trunk you register once, then sync into your account: ```python theme={null} summary = await client.numbers.sync() # {"synced": int, "new": int} ``` Full setup: [Trunks](/speech-stack/numbers#trunks). The `agent_id` on the pipe (`"browser-playground"`) is the same string your `AgentRunner` registers under in the Quickstart. They must match exactly, or inbound calls never reach your runner. See [IDs You'll Meet](/get-started/core-concepts#ids-youll-meet). ## Step 2 - Answer an inbound call Same shape as the Quickstart agent - one `entrypoint`, one `AgentRunner`. The brain here is SuperDialog's `LLMAgent` instead of the Anthropic adapter; either works, and nothing about the call path changes: ```python theme={null} # agent.py - standalone runner, no playground server needed import os from unpod import AgentRunner, CallContext from superdialog import LLMAgent async def entrypoint(ctx: CallContext) -> None: ctx.session.dialog_machine = LLMAgent( llm="anthropic/claude-haiku-4-5-20251001", system_prompt="You are a helpful voice assistant. Keep answers under 3 sentences.", ) await ctx.session.run() def build_runner() -> AgentRunner: return AgentRunner( entrypoint=entrypoint, agent_id=os.getenv("AGENT_ID", "browser-playground"), # base_url and api_key derive from UNPOD_BASE_URL / UNPOD_API_KEY # unless you pass explicit args here. ) if __name__ == "__main__": build_runner().start() # blocking ``` Start the runner: ```bash theme={null} python agent.py ``` The runner connects to the orchestrator and waits. Now call the number you attached. Unpod recognises the number, looks up its pipe, sees the pipe's `agent_id`, and dispatches the call to your waiting runner. Your agent answers and speaks - the same brain you heard in the browser, now on the phone. The runner does not need to restart when you provision the number. Run `setup.py` once, then leave the runner up; it serves every inbound call until you stop it. ## Step 3 - Make an outbound call (optional) Inbound is one direction. To have your agent place a call, use `calls.create` with the pipe and the destination number: ```python theme={null} # call_out.py - dispatch an outbound call import asyncio from unpod import AsyncClient PIPE_ID = "pipe_..." # from setup.py TO_NUMBER = "+19995550001" async def make_call() -> None: async with AsyncClient() as client: call = await client.calls.create( pipe_id=PIPE_ID, to_number=TO_NUMBER, ) print(f"Outbound call {call.call_id} -> {TO_NUMBER} (status: {call.status})") asyncio.run(make_call()) ``` `calls.create` enqueues the call and returns immediately with `status="pending"`. Unpod dials out, then dispatches the answered call to the same running `AgentRunner` - your `entrypoint` handles outbound exactly as it handles inbound. You can dispatch by agent instead of by pipe: pass `agent_id=` to `calls.create` and omit `pipe_id`. Unpod resolves a pipe bound to that `agent_id` server-side (`agent_id` wins if you pass both). `to_number` is always required. ## Next steps The full path: trunks, numbers, recording, and deployment. Campaigns, dynamic instructions, and per-call data. Plug in LangChain, an HTTP endpoint, or any brain you already have. # Getting Started Source: https://docs.unpod.ai/get-started/getting-started Your first request against a trained agent, in about 2 minutes. ## Prerequisites * An **endpoint key** - publish a playbook, then open **Deploy as Endpoint → Manage API Keys** in the [Playground](https://superdialog.unpod.ai/playground). * Basic REST knowledge. That's it - no SDK required for this page. ## Authentication Requests to the Chat API carry one header: ``` Authorization: Bearer $UNPOD_API_KEY ``` Unpod has two credentials, and they are not interchangeable: | You are calling | Host | Header | Key from | | --------------------------------------- | -------------------- | ----------------------- | ----------------------------------------------- | | **Chat API** (this page) | `inference.unpod.ai` | `Authorization: Bearer` | Playground → Deploy as Endpoint | | **Python SDK** (pipes, numbers, runner) | `api.unpod.ai` | handled by the SDK | [unpod.ai/api-keys](https://unpod.ai/api-keys/) | The SDK reads its key from `UNPOD_API_KEY` too - set whichever one matches the path you are on. ## Your first request Every trained agent is callable over the same [chat/completions](/playbook/api) shape you already know. `model` is the agent - here, a public example playbook: ```bash theme={null} curl https://inference.unpod.ai/v1/chat/completions \ -H "Authorization: Bearer $UNPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"public:PB_7ZRMzCA1ojQ9LlcK","messages":[{"role":"user","content":"hi"}]}' ``` ```json theme={null} { "choices": [ { "message": { "role": "assistant", "content": "Hi! Welcome to Lumina Spa - I'm Mira, your booking assistant. How can I help you today?" }, "finish_reason": "stop" } ] } ``` Not a generic reply - a trained persona, following its own playbook. ## Keep the conversation Add a `user` id and the agent remembers the thread across requests: ```bash theme={null} curl https://inference.unpod.ai/v1/chat/completions \ -H "Authorization: Bearer $UNPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"public:PB_7ZRMzCA1ojQ9LlcK","messages":[{"role":"user","content":"hi"}],"user":"sess_abc"}' ``` ## Go further Describe your use case in the Playground; the playbook writes itself. Drop the endpoint into LiveKit, Pipecat, or any chat workflow. Give it a voice - speech in, speech out, brain included. No code - publish straight to a live phone number. # Phone Source: https://docs.unpod.ai/get-started/phone Your trained agent, on a live phone number. Nothing to host. Everything managed - brain, speech, telephony. Describe the agent, hear it talk, publish it to a live number. No code, and no account needed to start. ## Go live in 3 steps ### 1. Describe the agent Open the [Playground](https://superdialog.unpod.ai/playground) and type one line - "Confirm an appointment - offer Friday 4pm, fall back to 5pm." The builder writes the playbook; the **Editor** tab shows it live. ### 2. Test by voice Switch to **Preview** and press to talk. What you hear in the Playground is what a caller hears - it runs on the same stack as production. Refine by chatting or editing, then **Save**. ### 3. Publish to a number Hit **Publish**, then **Deploy as Voice Agent** in the deploy drawer. Pick a number, deploy, dial it. The speech pipe is provisioned automatically. Full walkthrough: [Publish & Share](/playbook/publish-and-share). ## Go deeper Clone a public agent and start from there. Simulate callers, tune automatically. Prefer code? Hear your own agent in the browser in 5 minutes. Wire a number and runner through the API. The full dashboard - spaces, analytics, knowledge base. Numbers, trunks, and provider integrations. # Quickstart Source: https://docs.unpod.ai/get-started/quickstart Talk to your own voice agent in the browser in about 5 minutes. No phone number needed. Talk to your own agent in the browser. **No phone number, no carrier account.** You write the brain; Unpod runs microphone capture, speech-to-text, text-to-speech, and the audio bridge. Want a real phone call instead? Go to [Make your first phone call](/get-started/first-phone-call). ## Prerequisites * Python 3.12+ * An **SDK key** (`sk_...`) from [unpod.ai/api-keys](https://unpod.ai/api-keys/). This is not the endpoint key the [Chat API](/playbook/api) uses - see [Getting started](/get-started/getting-started#authentication). * An LLM provider key (any provider; this page uses Anthropic) ## Step 1 - Install ```bash theme={null} pip install "unpod[dialog]" ``` ```bash theme={null} uv add "unpod[dialog]" ``` ## Step 2 - Set two keys ```bash .env theme={null} UNPOD_API_KEY="sk_..." ANTHROPIC_API_KEY="sk-ant-..." ``` That is everything the default setup needs. `UNPOD_API_KEY` authenticates you; the provider key is read by whichever LLM you plug in. The SDK does not read `.env` by itself. Load it with `load_dotenv()` (as the snippets below do) or export the variables in your shell - either way, before you construct `AsyncClient()` or `AgentRunner()`, which is when the SDK reads them. Set these two instead of the defaults. `UNPOD_BASE_URL` does not work for local services. ```bash theme={null} UNPOD_SERVICE_BASE_URL=http://localhost:8000/platform UNPOD_ORCHESTRATOR_URL=ws://localhost:8000 ``` The hosted default is `UNPOD_BASE_URL="api.unpod.ai"`. The SDK derives REST (`https:///platform`) and the orchestrator (`wss://`) from that one host, so you set it once. ## Step 3 - Create a speech pipe A **pipe** ties a voice to your agent. Run this once: ```python setup.py theme={null} from dotenv import load_dotenv load_dotenv(override=True) import asyncio from unpod import AsyncClient async def main(): async with AsyncClient() as client: profiles = await client.voice_profiles.list(language="en") pipe = await client.pipes.create( name="Support Bot", voice_profile=profiles[0].profile_id, agent_id="my-support-agent", ) print("Pipe ID:", pipe.pipe_id) asyncio.run(main()) ``` What it does: 1. **`voice_profiles.list()`** - the catalog of STT + TTS bundles. Pick any; swap later without touching your agent. See [Voice profiles](/speech-stack/voice-profiles). 2. **`pipes.create()`** - binds that voice to an `agent_id`. **`agent_id` is a string you choose**, and it must be identical here and in your `AgentRunner` below. A mismatch is the most common reason a call never reaches your code. Save the printed `pipe_id` - Step 5 needs it. ## Step 4 - Write the agent ```python agent.py theme={null} from dotenv import load_dotenv load_dotenv(override=True) from anthropic import AsyncAnthropic from unpod import AgentRunner, CallContext from unpod.adapters import AnthropicAdapter async def handle_call(ctx: CallContext) -> None: ctx.session.dialog_machine = AnthropicAdapter( client=AsyncAnthropic(), model="claude-haiku-4-5-20251001", system_prompt="You are Alex, a friendly support agent.", ) await ctx.session.run() AgentRunner(entrypoint=handle_call, agent_id="my-support-agent").start() ``` What each part does: 1. **`handle_call`** - runs once per conversation. `ctx` carries the call; the `session` is how you act on it. 2. **`ctx.session.dialog_machine`** - your brain. Any adapter works here: Anthropic, OpenAI, LangChain, a plain HTTP endpoint, or a SuperDialog playbook. See [Bring your agent](/speech-stack/bring-your-agent). 3. **`session.run()`** - hands each transcribed turn to the brain and speaks the reply back. 4. **`AgentRunner(...).start()`** - registers with Unpod and waits for calls. It reads `UNPOD_API_KEY` from the environment. ```python theme={null} from superdialog import DialogMachine async def handle_call(ctx: CallContext) -> None: ctx.session.dialog_machine = DialogMachine( "support.yaml", llm="anthropic/claude-haiku-4-5-20251001" ) await ctx.session.run() ``` Checkpoints, slots, and tools instead of one long prompt - see [Thinking in playbooks](/superdialog/thinking-in-playbooks). ## Step 5 - Talk to it Start the runner and leave it running: ```bash theme={null} python agent.py ``` You should see it register and wait for calls: ``` AgentRunner registered agent_id=my-support-agent transport=dial_out Waiting for calls... ``` Now open the Playground in your browser and press to talk: Microphone capture, the session token exchange, and the audio bridge - no local UI to run. Prefer to keep it in the terminal? `superdialog playbook chat --playbook x.yaml` talks to a playbook with no infrastructure at all - see [CLI](/superdialog/cli). Your backend mints a short-lived token per user and the frontend connects with it: ```python theme={null} from unpod import AsyncClient async def get_session_token(pipe_id: str, user_id: str) -> str: async with AsyncClient() as client: token = await client.sessions.create_token( pipe_id=pipe_id, metadata={"user_id": user_id}, ) return token.token # single-use; check token.expires_at ``` `@unpod/web-sdk` is **not yet published on npm**, so browser embedding is not available today. To run the local playground harness from source, see [Run it locally](/playbook/developer-setup). ## What Unpod owns vs what you own | Unpod owns | You own | | ------------------------------ | ------------------------ | | STT, TTS, voice profiles | The prompt or playbook | | Audio transport and the bridge | Tools and business logic | | Routing, threading, retries | Customer data and memory | | Numbers and telephony | Model choice | ## Next Put this same agent on a real number. LangChain, an HTTP webhook, or any brain you already have. Capacity, scaling, and graceful shutdown. # Realtime Source: https://docs.unpod.ai/get-started/realtime Your trained agent, over one speech-in speech-out session. Brain included. Like a realtime voice API - except the brain is already trained on your use case. The full speech loop (STT, VAD, barge-in, TTS) plus your agent, served as one session. Your app or telephony connects; Unpod runs the conversation. ## How it connects A **Speech Pipe** bundles a voice profile with an agent. Sessions reach it from the web over WebSocket or from a phone number over SIP - same agent, same behaviour, either way. ## Go live in 3 steps ### 1. Create a Speech Pipe ```python theme={null} pipe = await client.pipes.create( name="Support Bot", voice_profile="vp_en_female_hd", # from voice_profiles.list() agent_id="my-bot", ) ``` ### 2. Connect a session ```javascript theme={null} import { UnpodSession } from "@unpod/web-sdk"; const session = new UnpodSession({ token }); // short-lived token from your backend await session.connect(); // mic + speaker, full duplex session.on("agent_reply", (text) => console.log("Agent:", text)); ``` Mint tokens server-side with `client.sessions.create_token(pipe_id=...)`. Protocol and auth: [WebSocket sessions](/speech-stack/websocket). `@unpod/web-sdk` is **not yet published on npm**. Until it ships, reach a browser session through the [Playground](https://superdialog.unpod.ai/playground?tab=preview), or connect over telephony. Attach a [number](/speech-stack/numbers) to the pipe - provision from Unpod or bring your own over a [trunk](/speech-stack/numbers#trunks). LiveKit SIP works too: [LiveKit integration](/telephony/integrations/livekit/api). Raw audio-frame WebSocket streaming for custom transports is on the [roadmap](/telephony/integrations/websockets). ### 3. Test it Call the number, or open your web session and talk. Recordings and transcripts land automatically - see [Recordings & Transcripts](/speech-stack/recordings-transcripts). ## Go deeper Session tokens, events, capabilities. STT + TTS bundles per language, with failover. The unit that wires voice, agent, and numbers. Point a pipe at any HTTP endpoint or LangChain brain. # Unpod vs Pipecat vs LiveKit Source: https://docs.unpod.ai/get-started/vs-competition How Unpod compares to audio frameworks on time to production and what you own. ## TL;DR | | Unpod | LiveKit | Pipecat | | ---------------------- | ------------------------------------------ | ------------------------------------------------ | ------------------------------------ | | **What it is** | Communication infra for AI agents | Real-time audio infrastructure + agent framework | Open-source voice pipeline framework | | **You own** | Your agent logic | Everything | Everything | | **They manage** | Phone numbers, STT/TTS, VAD, orchestration | WebRTC rooms (self-hosted or cloud) | Nothing - it's a library | | **Phone numbers** | Built-in, no carrier account | Bring your own SIP trunk | Bring your own | | **STT/TTS** | Fully managed, automatic failover | You configure and run providers | You configure and run providers | | **Time to first call** | \~2 hours | \~4 months | \~4 months | | **Scaling** | Automatic | You manage workers | You manage processes | | **Your agent format** | Webhook, SDK, LangChain, MCP (preview) | LangChain, custom | LangChain, custom | | **Best for** | Teams that want calling infra handled | Teams needing full audio control | Teams wanting pipeline flexibility | *** ## The Core Difference LiveKit and Pipecat are **audio frameworks**. They give you building blocks to construct a voice pipeline. You choose and wire every component - STT provider, TTS provider, VAD, transport, endpointing. Then you deploy it, scale it, and keep it running. Unpod is **calling infrastructure**. You bring the agent. We give it a phone number and handle all the audio plumbing. *** ## Pipecat Pipecat gives you components to build a voice pipeline yourself: ```python theme={null} # Pipecat - you wire and run every component pipeline = Pipeline([ transport.input(), stt, llm, tts, transport.output(), ]) runner = PipelineRunner() await runner.run(PipelineTask(pipeline)) ``` You pick the STT, the TTS, the transport. You manage the process. You handle failover. Maximum flexibility - maximum setup work. **Good fit:** Teams with specific provider requirements or non-standard pipeline shapes. Research and experimental systems. *** ## LiveKit LiveKit provides real-time infrastructure (WebRTC rooms, SIP, TURN) and an agent framework on top: ```python theme={null} # LiveKit - you configure all providers and manage the room async def entrypoint(ctx: JobContext): await ctx.connect() agent = VoiceAgent( vad=silero.VAD.load(), stt=deepgram.STT(), llm=openai.LLM(), tts=cartesia.TTS(), ) agent.start(ctx.room) ``` You configure every provider. You deploy and manage the LiveKit server (or pay for LiveKit Cloud). Phone numbers require a SIP trunk from a carrier. **Good fit:** Teams already on LiveKit, or needing fine-grained WebRTC control for browser and mobile use cases beyond phone calls. *** ## Unpod You bring the agent. Unpod gives it a phone number and handles every audio layer: ```python theme={null} # Unpod - you write the logic, we handle the communication stack async def handle_call(ctx: CallContext) -> None: ctx.session.dialog_machine = DialogMachine("support.yaml", llm="anthropic/claude-haiku-4-5") await ctx.session.run() AgentRunner(entrypoint=handle_call, agent_id="agt_...").start() ``` Numbers provisioned from Unpod. STT/TTS configured via voice profiles with automatic failover. Orchestration and dispatch handled automatically. You can also point Unpod at an existing HTTP endpoint or LangChain agent without writing any SDK code. **Good fit:** Teams that want production voice calls without becoming telephony experts. *** ## When to Choose Each ### Choose Unpod when: * You want phone numbers without a carrier account or SIP trunk * You have an existing agent and want to give it voice in hours - not months * You do not want to manage STT/TTS provider accounts, failover, or audio infrastructure * Your core value is the agent logic - not the communication stack ### Choose LiveKit when: * You need granular WebRTC control (custom ICE, codec requirements) * You are already running LiveKit infrastructure * You need browser or mobile real-time communication beyond phone calls * You want full ownership of every infrastructure component ### Choose Pipecat when: * You need a highly customised pipeline shape or experimental architecture * You want maximum control over every processing step * You are comfortable managing deployment, scaling, and provider accounts yourself *** ## Feature Comparison ### Telephony | Feature | Unpod | LiveKit | Pipecat | | ---------------------------- | --------- | -------------- | -------------- | | Managed phone numbers | Yes | No | No | | SIP trunk required | No | Yes | Yes | | Inbound calls | Yes | Yes (with SIP) | Requires setup | | Outbound calls | Yes (SDK) | Yes (with SIP) | Requires setup | | BYON (bring your own number) | Yes | Yes | Yes | ### Voice Processing | Feature | Unpod | LiveKit | Pipecat | | ----------------- | -------------------- | ----------------------- | ----------------------- | | Managed STT/TTS | Yes - voice profiles | No - configure yourself | No - configure yourself | | Provider failover | Automatic | Manual | Manual | | VAD + barge-in | Managed | You configure | You configure | | Deepgram STT | Yes | Yes | Yes | | Cartesia TTS | Yes | Yes | Yes | | ElevenLabs TTS | Yes | Yes | Yes | ### Developer Experience | Feature | Unpod | LiveKit | Pipecat | | ------------------------------ | ------- | ------------ | ------------ | | Python SDK | Yes | Yes | Yes | | HTTP endpoint / webhook | Yes | No | No | | Structured flows (SuperDialog) | Yes | No | No | | Call recordings | Managed | Self-managed | Self-managed | | Transcripts | Managed | Self-managed | Self-managed | | Dashboard + UI | Yes | Limited | No | | Metrics + analytics | Yes | Limited | No | *** ## Open Source All three are open source. Unpod's core components - `unpod`, `supervoice`, `superdialog` - are on GitHub. You can self-host the full stack or use Unpod's managed cloud. Run the full Unpod communication stack on your own infrastructure. # Why Unpod Source: https://docs.unpod.ai/get-started/why-unpod The intelligence layer for realtime AI. Train an agent on your use case. Use it like a model - over chat, realtime voice, or a phone number. ## Why realtime agents fail Speech is solved. Telephony is solved. The layer that runs the conversation is where projects stall. Every top model drops \~39% from single-turn to multi-turn - mostly unreliability, not capability. Take a wrong turn and the conversation never recovers. Prompt-tuning plateaus around 85%: nothing in a 100-page prompt is addressable, so nothing is testable. Humans swap turns in \~200ms; past \~700ms a caller hears a machine. The LLM is \~70% of that budget, and frontier models spend 1.1-1.4s before their first token - the whole budget, on one hop. ## What Unpod runs Conversations as checkpoints and outcomes, not prose. Every step is addressable - so it can be simulated, fixed, and regression-tested. Each turn gets only the context it needs - so the agent stays fast on a third-party model, or faster on a fine-tuned \~1B SLM trained on your workflows. Swap either way without touching the conversation. Pass a session id and the agent keeps its state - across turns, and across calls. ## Use it like a model | Surface | I/O | Live in | | --------------------------------- | ------------------- | ----------- | | [Chat](/get-started/chat) | text in, text out | 1 URL swap | | [Realtime](/get-started/realtime) | audio in, audio out | 1 WebSocket | | [Phone](/get-started/phone) | a phone number | 1 click | Same trained agent, three ways in. Your orchestration, speech, and transport stay where they are.
Where Unpod sits
Layer 3
Agent platforms Vapi / Retell / Bland
used by
Layer 2
Unpod Communication infra
builds on
Layer 1
Raw telephony Twilio / Plivo / Bandwidth
## Start building Your first request, in about 2 minutes. Try it Playground Build and hear an agent in the browser. No account needed. Open source, MIT. Self-host the full stack. # Getting Started with the Unpod API Source: https://docs.unpod.ai/guides/getting-started-with-unpod-api Authenticate, create tasks, and trigger AI voice calls via the Unpod REST API - with real curl examples **Last updated:** July 22, 2026 · **API version:** v2 · **Tested against:** `openapi.yaml` 2.0.0 · 8 min read The legacy `https://api.unpod.ai/api/v1/` host and `Authorization: Bearer` scheme are **deprecated**. All current endpoints use the production host `https://unpod.ai/`, the `/api/v2/platform/` path prefix, and `Authorization: Token` authentication. See the [Authentication guide](/api/get-started/authentication). *** Unpod exposes a full REST API so you can build, automate, and integrate AI voice agents into any system - CRMs, helpdesks, internal tools, or custom workflows. This guide walks you through everything from getting your API key to making your first outbound call. ## Prerequisites * An Unpod account at [unpod.ai](https://unpod.ai) * At least one configured AI agent * A telephony bridge with a phone number assigned * Your API key (from **AI Studio → API Keys**) *** ## Step 1: Authenticate All Unpod API requests use **Token** authentication. Add your API key to every request header. Many endpoints also require the `Org-Handle` header (your organization domain handle). ```bash theme={null} curl "https://unpod.ai/api/v2/platform/organizations/" \ -H "Authorization: Token YOUR_API_KEY" \ -H "Org-Handle: your-org-handle" ``` Response: ```json theme={null} { "count": 3, "status_code": 200, "message": "Organizations fetched successfully", "data": [ { "id": 1, "name": "Unpod TV", "domain_handle": "unpod.tv", "created_at": "2024-01-15T10:30:00Z" } ] } ``` Keep your key secret - it has full access to your workspace. *** ## Step 2: Get Your Space Token Spaces are the core organizational unit in Unpod. Most API calls are scoped to a space. ```bash theme={null} curl "https://unpod.ai/api/v2/platform/spaces/" \ -H "Authorization: Token YOUR_API_KEY" \ -H "Org-Handle: your-org-handle" ``` Response: ```json theme={null} { "count": 207, "status_code": 200, "message": "Spaces fetched successfully", "data": [ { "id": "sp_001", "name": "Sales Outreach Q1", "token": "8KZAMRAHSXXXXXXMAYNASMJC", "agent_handle": "space-agent-8qmk42nslp91wrh3dz7btxc4", "created_at": "2026-01-10T08:00:00Z" } ] } ``` Save the `token` value - you'll use it to scope task and run lookups. *** ## Step 3: List Your Agents Fetch all agents configured in your organization: ```bash theme={null} curl "https://unpod.ai/api/v2/platform/agents/" \ -H "Authorization: Token YOUR_API_KEY" \ -H "Org-Handle: your-org-handle" ``` Response: ```json theme={null} { "count": 94, "status_code": 200, "message": "Agents fetched successfully", "data": [ { "handle": "space-agent-8qmk42nslp91wrh3dz7btxc4", "name": "General Agentic", "type": "Voice", "state": "published", "purpose": "Handle outbound sales calls" } ] } ``` Note the `handle` field of the agent (pilot) you want to use for calls. *** ## Step 4: Create a Task (Outbound Call) A **Task** triggers an outbound voice call from your AI agent to one or more contacts. Tasks are created inside a space; the request body takes the agent `pilot` handle and a `documents` array of contacts. ```bash theme={null} curl -X POST "https://unpod.ai/api/v2/platform/spaces/8KZAMRAHSXXXXXXMAYNASMJC/tasks/create/" \ -H "Authorization: Token YOUR_API_KEY" \ -H "Org-Handle: your-org-handle" \ -H "Content-Type: application/json" \ -d '{ "pilot": "space-agent-8qmk42nslp91wrh3dz7btxc4", "context": "Call the lead and discuss the project requirements.", "schedule": { "type": "now" }, "documents": [ { "name": "John Doe", "email": "john@example.com", "contact_number": "1234567890", "context": "Follow up on proposal sent last week", "labels": ["warm-lead", "webinar"] } ] }' ``` Response: ```json theme={null} { "status_code": 200, "message": "Task Created Successfully", "data": { "run_id": "R74802366fe9011f0878d43cd8a99e069", "task_ids": ["T74802367fe9011f0878d43cd8a99e069"], "status": "pending" } } ``` The API returns a `run_id` and `task_ids`. The agent will call the number within seconds. *** ## Step 5: Track the Run Once a task is created, Unpod creates a **Run** - the actual call execution. Poll runs in the space to track status: ```bash theme={null} curl "https://unpod.ai/api/v2/platform/spaces/8KZAMRAHSXXXXXXMAYNASMJC/runs/" \ -H "Authorization: Token YOUR_API_KEY" \ -H "Org-Handle: your-org-handle" ``` Response: ```json theme={null} { "count": 9, "status_code": 200, "message": "Runs Fetched Successfully", "data": [ { "run_id": "Recac64fe03e911f1878d43cd8a99e069", "run_mode": "prefect", "status": "completed", "created": "2026-02-07T05:57:45Z", "modified": "2026-02-07T05:57:45Z" } ] } ``` For per-task detail (transcript, recording, outcome) within a run: ```bash theme={null} curl "https://unpod.ai/api/v2/platform/spaces/8KZAMRAHSXXXXXXMAYNASMJC/runs/Recac64fe03e911f1878d43cd8a99e069/tasks/" \ -H "Authorization: Token YOUR_API_KEY" \ -H "Org-Handle: your-org-handle" ``` *** ## Step 6: Fetch Call Logs After a call completes, retrieve call detail records (CDR) - including transcript, recording, and post-call analysis: ```bash theme={null} curl "https://unpod.ai/api/v2/platform/cdr/?call_type=outbound&page=1&page_size=20" \ -H "Authorization: Token YOUR_API_KEY" \ -H "Org-Handle: your-org-handle" ``` Call logs include: duration, transcript, sentiment, outcome, and any extracted data from your agent's analysis config. *** ## Passing Context to Your Agent Use the per-contact `context` field (inside each `documents` item) or the task-level `context` to pass call-specific information. Your agent's system prompt can reference this data via template variables - useful for personalizing conversations: ```json theme={null} { "context": "Q1 renewal outreach", "documents": [ { "name": "Jane", "contact_number": "1234567890", "context": "Premium tier customer, last order 2026-05-20" } ] } ``` *** ## Rate Limits | Plan | Calls per minute | Tasks per day | | ---------- | ---------------- | ------------- | | Free | 5 | 50 | | Pro | 60 | 5,000 | | Enterprise | Custom | Unlimited | *** ## What's Next Full endpoint reference with request/response schemas. API key management and security best practices. List, inspect, and manage agents programmatically. Track call execution status and outcomes. # Guides Source: https://docs.unpod.ai/guides/index Tutorials, developer guides, and product insights from the Unpod team **Last updated:** July 22, 2026 · **Product version:** v2 ## Unpod Guides Tutorials, developer guides, and product insights for building AI voice agents. **May 27, 2026 · 8 min read** Authenticate, create your first task, and make an outbound AI voice call - all via the REST API. Step-by-step with real curl examples. **May 27, 2026 · 10 min read** From blank canvas to a live, phone-ready AI agent. Covers identity, persona, voice profile, knowledge base, and telephony setup. **May 27, 2026 · 12 min read** Deploy the full Unpod stack on your own infrastructure using Docker Compose. Covers config, secrets, migrations, and production hardening. **May 27, 2026 · 7 min read** How Unpod routes calls through bridges, SIP providers, and phone numbers - and how to configure it for your use case. **May 27, 2026 · 11 min read** Build a polling service and webhook receiver to capture Unpod call transcripts, summaries, and AI analysis - then push to HubSpot, Slack, or your database. # Self-Hosting Unpod: Complete Open Source Guide Source: https://docs.unpod.ai/guides/self-hosting-unpod Deploy the full Unpod stack on your own infrastructure using Docker Compose - config, secrets, migrations, and production hardening **Last updated:** July 22, 2026 · **Product version:** v2 (self-hosted OSS) · 12 min read *** Unpod is fully open source under the MIT license. This means you can run the entire platform - frontend, backend, voice pipeline, and all infrastructure - on your own servers. This guide covers everything from local setup to a production-hardened deployment. ## Why Self-Host? * **Data sovereignty**: Call recordings and transcripts never leave your infrastructure * **Compliance**: Meet HIPAA, GDPR, or SOC 2 requirements with full control over data residency * **Cost at scale**: For high call volumes, self-hosting can significantly reduce per-minute costs * **Customization**: Modify the source, add integrations, and white-label the platform *** ## Architecture Overview Unpod is a monorepo with these key services: | Service | Tech | Port | | ------------- | --------------- | ----- | | Frontend | Next.js | 3000 | | Backend Core | Django (Python) | 8000 | | API Services | FastAPI | 9116 | | Real-time | Centrifugo | 8100 | | Database | PostgreSQL | 5432 | | Cache / Queue | Redis | 6379 | | Storage | MongoDB | 27017 | All services are containerized and orchestrated via Docker Compose. *** ## Prerequisites Install these before you start: ```bash theme={null} # Check versions node --version # v20+ python3 --version # 3.11+ docker --version # 24+ docker compose version # v2.20+ git --version ``` *** ## Quick Start (Development) The fastest path to a running instance: ```bash theme={null} git clone https://github.com/unpod-ai/unpod.git cd unpod make quick-start # installs deps, starts Docker, runs migrations make dev # starts all dev servers ``` Access points after startup: | Service | URL | | -------- | ------------------------------------------------------------------------ | | App | [http://localhost:3000](http://localhost:3000) | | API | [http://localhost:8000/api/v1/](http://localhost:8000/api/v1/) | | Admin | [http://localhost:8000/unpod-admin/](http://localhost:8000/unpod-admin/) | | API Docs | [http://localhost:9116/docs](http://localhost:9116/docs) | Default credentials: `admin@unpod.ai` / `admin123` *** ## Environment Configuration Copy the example env file and set your values: ```bash theme={null} cp .env.example .env ``` Critical variables to configure: ```bash theme={null} # Django SECRET_KEY=your-long-random-secret-key DEBUG=False ALLOWED_HOSTS=your-domain.com,www.your-domain.com # Database POSTGRES_DB=unpod POSTGRES_USER=unpod POSTGRES_PASSWORD=strong-password-here DATABASE_URL=postgresql://unpod:password@postgres:5432/unpod # Redis REDIS_URL=redis://redis:6379/0 # Centrifugo (real-time) CENTRIFUGO_SECRET=centrifugo-secret-token CENTRIFUGO_API_KEY=centrifugo-api-key # AI Providers (at least one required) OPENAI_API_KEY=sk-... GROQ_API_KEY=gsk_... # Voice Providers LIVEKIT_URL=wss://your-livekit-instance.com LIVEKIT_API_KEY=API_KEY LIVEKIT_SECRET=SECRET ``` *** ## Production Deployment with Docker Compose Use the production compose file: ```bash theme={null} docker compose -f docker-compose.yml up -d --build ``` ### Run Database Migrations ```bash theme={null} docker compose exec backend python manage.py migrate --no-input docker compose exec backend python manage.py collectstatic --no-input ``` ### Create Admin User ```bash theme={null} docker compose exec backend python manage.py createsuperuser ``` *** ## Reverse Proxy Setup (Nginx) Put Nginx in front of the app for SSL termination and routing: ```nginx theme={null} server { listen 443 ssl; server_name your-domain.com; ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem; # Frontend location / { proxy_pass http://localhost:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # Backend API location /api/ { proxy_pass http://localhost:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # WebSocket (Centrifugo) location /connection/websocket { proxy_pass http://localhost:8100; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } } ``` Use Certbot for free SSL: `certbot --nginx -d your-domain.com` *** ## Production Hardening Checklist Before going live: * [ ] Change all default passwords and secret keys * [ ] Set `DEBUG=False` in all environments * [ ] Configure `ALLOWED_HOSTS` to your domain only * [ ] Enable PostgreSQL SSL connections * [ ] Set up automated database backups (pg\_dump to S3 or equivalent) * [ ] Configure log rotation for Django and Nginx logs * [ ] Set up health check endpoints for uptime monitoring * [ ] Restrict Redis to internal network only (no public exposure) * [ ] Use Docker secrets or a vault for API keys (not plain .env in production) *** ## Updating Unpod ```bash theme={null} git pull origin main docker compose -f docker-compose.yml up -d --build docker compose exec backend python manage.py migrate --no-input ``` Check the [GitHub releases](https://github.com/unpod-ai/unpod/releases) for breaking changes before pulling. *** ## What's Next All environment variables and their defaults. Monorepo structure, service dependencies, and data flow. Step-by-step local setup with all three setup methods. Configure agents and telephony after setup. # Telephony Deep Dive: Bridges, Numbers & Providers Source: https://docs.unpod.ai/guides/understanding-telephony How Unpod routes calls through bridges, SIP providers, and phone numbers - and how to configure it for your use case **Last updated:** July 22, 2026 · **API version:** v2 · **Tested against:** `openapi.yaml` 2.0.0 · 7 min read API examples use the production host `https://unpod.ai/`, the `/api/v2/platform/` path prefix, and `Authorization: Token`. The legacy `api.unpod.ai/api/v1/` + `Bearer` scheme is deprecated. *** Telephony is the layer that connects your AI agent to the real phone network. Unpod abstracts away the complexity of SIP, PSTN, and WebRTC - but understanding how the pieces fit together helps you configure it correctly and debug issues faster. ## The Three Components Unpod telephony has three core concepts: Animated telephony routing flow diagram showing a phone number routed through a bridge and provider to an agent. | Component | What It Is | Example | | ---------------- | ------------------------------------------------ | ---------------- | | **Phone Number** | A real PSTN/VoIP number | +1 415 555 0100 | | **Bridge** | Routing config linking number + provider + agent | `support-bridge` | | **Provider** | Voice infrastructure that handles media | LiveKit, Vapi | *** ## Providers: The Voice Infrastructure Providers handle the actual audio - WebRTC media servers, transcription, and TTS streaming. ### LiveKit LiveKit is an open-source, WebRTC-based media server. Unpod's real-time voice pipeline runs on LiveKit. * Sub-300ms end-to-end latency * Can be self-hosted (cost control at scale) * Supports SIP trunking for PSTN connectivity * Best for: developers who want full control, self-hosted deployments ### Vapi Vapi is a hosted voice AI infrastructure platform. * Fully managed - no infrastructure to run * Built-in SIP trunking and number management * Simplified setup for faster time to production * Best for: teams that want managed infra without ops overhead *** ## Bridges: The Routing Layer A Bridge is a named configuration that connects: * **One provider** (LiveKit or Vapi) * **One or more agents** * **One or more phone numbers** Think of it as a switchboard. When a call arrives at a number, the bridge decides which agent handles it. ### Creating a Bridge via Dashboard Go to **Dev Platform → Telephony → Bridges → Create Bridge**: ```json theme={null} { "name": "support-bridge", "provider": "livekit", "agent_handle": "aria-support", "inbound_enabled": true, "outbound_enabled": true } ``` ### Creating a Bridge via API ```bash theme={null} curl -X POST "https://unpod.ai/api/v2/platform/telephony/bridges/" \ -H "Authorization: Token YOUR_API_KEY" \ -H "Org-Handle: your-org-handle" \ -H "Content-Type: application/json" \ -d '{ "name": "Support Bridge", "slug": "support-bridge" }' ``` A bridge is created with just a `name` and `slug`. Attach a provider afterward with the [connect-provider](/api/telephony/connect-provider-to-bridge) endpoint (`POST /api/v2/platform/telephony/bridges/{slug}/connect-provider/`). *** ## Phone Numbers Unpod provisions real phone numbers through your connected SIP provider. Numbers are then assigned to bridges. ### Number Types | Type | Description | Use Case | | ------------- | ---------------------- | ------------------------ | | Local DID | Local area code number | Customer support, sales | | Toll-Free | 800/888/877 numbers | Enterprise support lines | | International | Non-US numbers | Global operations | ### Listing Your Numbers Numbers are provisioned through your connected SIP provider (via the Dashboard or your provider's portal), then attached to trunks/bridges. The API exposes numbers as read-only - list them with: ```bash theme={null} curl "https://unpod.ai/api/v2/platform/telephony/numbers/" \ -H "Authorization: Token YOUR_API_KEY" \ -H "Org-Handle: your-org-handle" ``` To attach numbers to a trunk, use the [attach-numbers](/telephony/trunks/attach-numbers) endpoint (`POST /api/v2/platform/telephony/trunks/{id}/attach-numbers/`). Once assigned to a bridge, a number is active and routed. *** ## Call Flow: What Happens on an Inbound Call 1. Caller dials your number 2. SIP provider receives the call, looks up the number's bridge assignment 3. Bridge routes call to the configured provider (LiveKit/Vapi) 4. Provider streams audio to the Unpod voice pipeline 5. Voice pipeline runs: STT → LLM (with your agent config) → TTS 6. Audio streams back to caller in real time 7. Call ends, transcript and metadata written to Call Logs Total setup latency target: **under 300ms** from first word to agent response start. *** ## Outbound Calls For outbound (agent-initiated) calls, create a Task via API: ```bash theme={null} curl -X POST "https://unpod.ai/api/v2/platform/spaces/8KZAMRAHSXXXXXXMAYNASMJC/tasks/create/" \ -H "Authorization: Token YOUR_API_KEY" \ -H "Org-Handle: your-org-handle" \ -H "Content-Type: application/json" \ -d '{ "pilot": "space-agent-8qmk42nslp91wrh3dz7btxc4", "context": "Support follow-up call", "schedule": { "type": "now" }, "documents": [ { "name": "John Doe", "contact_number": "14155550100" } ] }' ``` Tasks are scoped to a space (`space_token` in the path) and take the agent `pilot` handle plus a `documents` array of contacts. Unpod dials each number through the space's bridge, and the agent begins the conversation. See the full [create-task reference](/api/execution/create-task). *** ## Multi-Agent Routing One bridge can route to different agents based on custom logic. Use the API to update bridge routing dynamically: ```bash theme={null} curl -X PATCH "https://unpod.ai/api/v2/platform/telephony/bridges/support-bridge/" \ -H "Authorization: Token YOUR_API_KEY" \ -H "Org-Handle: your-org-handle" \ -H "Content-Type: application/json" \ -d '{ "name": "Support Bridge — Billing Hours" }' ``` The bridge `PATCH` endpoint updates bridge fields such as `name`. To change which provider/agent a bridge routes to, use the [connect-provider](/api/telephony/connect-provider-to-bridge) / [disconnect-provider](/api/telephony/disconnect-provider-from-bridge) endpoints. Combine this with your own IVR or pre-call webhook to build intelligent call routing before the AI agent picks up. *** ## Debugging Common Issues | Symptom | Likely Cause | Fix | | --------------------------------------- | --------------------------------------- | ------------------------------------------------------------ | | Call connects but agent doesn't respond | Provider not reachable or misconfigured | Check LiveKit URL and API key in env | | Number provisioning fails | No SIP provider connected to bridge | Connect a provider first | | High latency on first word | TTS model cold start | Use a faster TTS model (OpenAI TTS > ElevenLabs for latency) | | Call drops after 30s | WebRTC ICE timeout | Check firewall rules for UDP 10000-60000 | *** ## What's Next Full API reference for bridges, numbers, and providers. Dashboard walkthrough for telephony setup. Configure and manage voice infrastructure providers. Trigger outbound AI calls via API. # Building Your First Voice AI Agent Source: https://docs.unpod.ai/platform/build-an-agent From blank canvas to a live, phone-ready AI agent - identity, persona, voice, knowledge base, and telephony **Last updated:** July 22, 2026 · **Product version:** v2 · **Tested against:** `openapi.yaml` 2.0.0 *** Voice AI agents are replacing traditional IVR systems and transforming how businesses handle calls. Unpod makes it possible to build a production-ready voice agent in under 30 minutes - no ML background required. This guide walks through every step. ## What We're Building A customer support agent that: * Answers inbound calls for a SaaS product * Has a defined persona and voice * Can look up information from uploaded docs * Hands off to a human when needed *** ## Step 1: Create Your Agent in AI Studio Log in to [unpod.ai](https://unpod.ai) and open **AI Studio**. Click **New Agent**. ### Identity Tab Set the core details: | Field | Example | | -------- | ------------------------------------------- | | Name | Aria | | Handle | `aria-support` (URL-safe slug, used in API) | | Language | English (US) | | Timezone | America/New\_York | The **handle** is how you reference this agent in the API and telephony config. *** ## Step 2: Define the Persona The persona determines how your agent thinks, speaks, and behaves. Open the **Persona** tab. ### System Prompt Write a clear system prompt that defines: * Role and purpose * Tone and communication style * What the agent should and should not do Example: ``` You are Aria, a friendly and professional customer support agent for Acme SaaS. Your goal is to help users resolve issues with their account, billing, and product features. Rules: - Always greet the caller by name if available - Be concise - callers are on the phone, not reading - If you cannot resolve an issue, offer to connect them with a human agent - Never make promises about refunds or SLA violations without checking policy ``` ### Greeting Message The first thing your agent says when the call connects: ``` Hi, you've reached Acme support. I'm Aria, your AI assistant. How can I help you today? ``` *** ## Step 3: Configure the Voice Profile Open the **Voice Profile** tab. Choose: * **TTS Provider**: ElevenLabs, OpenAI, Azure, or Google * **Voice**: Pick from available voices for your chosen provider * **Speed**: 0.9-1.0 works well for support calls * **Stability / Similarity**: Higher stability = more consistent, lower = more expressive Test voices using the built-in preview before finalizing. *** ## Step 4: Choose Your AI Model Under the **Advanced** tab, select the LLM powering your agent: | Provider | Model | Best For | | ------------ | ---------------- | ------------------------------ | | OpenAI | gpt-4o | General purpose, high accuracy | | Groq | llama-3.1-70b | Low latency, cost-efficient | | Google | gemini-1.5-flash | Multimodal, long context | | Azure OpenAI | gpt-4o | Enterprise compliance | For support bots, `gpt-4o` or `llama-3.1-70b` via Groq are solid starting points. *** ## Step 5: Add a Knowledge Base Upload product documentation, FAQs, or policy docs so your agent can answer accurately. Open **Knowledge Base** → **Upload Document**. Supported formats: * PDF, DOCX, TXT, Markdown * Web URLs (Unpod crawls and indexes the content) Unpod uses RAG (Retrieval-Augmented Generation) - the agent searches relevant chunks before generating a response. **Tips for better retrieval:** * Break large PDFs into topic-focused sections * Include an FAQ document with exact question phrasing customers use * Add a "what not to say" policy document to constrain agent behavior *** ## Step 6: Set Up Telephony Your agent needs a phone number to receive calls. ### 6a: Create a Bridge Go to **Telephony → Bridges → Create Bridge**. A bridge connects your agent to a voice infrastructure provider. * **Name**: `support-bridge` * **Provider**: LiveKit or Vapi * **Agent**: Select `aria-support` ### 6b: Add a Phone Number Go to **Numbers → Provision Number**. Select a country and area code. Unpod provisions a real phone number via your connected SIP provider. ### 6c: Link Number to Bridge Assign the provisioned number to your bridge. Incoming calls to this number will now route to Aria. *** ## Step 7: Test the Agent 1. Click **Preview** in AI Studio to test via the browser interface 2. Call the provisioned phone number from your mobile 3. Check **Call Logs** to review the transcript and see how the agent performed *** ## Monitoring & Iteration After going live: * **Call Logs**: Review transcripts to find gaps in knowledge base coverage * **Analytics**: Track call duration, resolution rate, and sentiment over time * **Analysis Tab**: Configure post-call extraction - pull structured data (intent, outcome, customer tier) from each conversation Iterate on the system prompt and knowledge base based on real call patterns. *** ## What's Next Full platform guide for business users. Trigger outbound calls to your agent via API. Detailed knowledge base configuration guide. Advanced telephony and provider setup. # Agents Source: https://docs.unpod.ai/platform/dev-platform/agents The heart of Unpod - shaping every conversation. ## Getting Started Unpod Agents are AI-powered assistants that handle calls, chats, and tasks. With an agent, you can decide how it interacts with users, what knowledge it accesses, and how it communicates over the phone. *** ## Identity Create a voice agent with a simple prompt, attach it with your phone number, and make your first call. ### Step 1 Login to the [dashboard](https://unpod.ai) of Unpod with your login credentials. Click on **AI Studio** from the left side of the dashboard as shown below. Identity Step 1 ### Step 2 Once you click on **AI Studio**, you will be redirected to the page from where you can start creating your first agent. Identity Step 2 ### Step 3 Now give the name to your agent according to your product or organization for which you are creating this agent. Identity Step 3 ### Step 4 Now fill the complete details of the agent in the highlighted fields. The fields which are mandatory to fill are **Description**, **Purpose**, **Classification**, also select whether the agent should be accessed by anyone (public) or accessed by only those whom you give the access (shared). For classification, there is a drop down menu from where you can select the tag according to your organization or product. You can also add the logo of your organization. In description, you can give the identity of the business. For example, I have created an agent for QuriousKid which is an educational institute. In Identity, I have mentioned: > "You are Ahaana, a friendly and understanding educational consultant from CuriousKid." Identity Step 4-1 Identity Step 4-2 ### Step 5 After filling all the details, click on **Save** button to create an agent. Identity Step 5 ### Step 6 Once you click on Save, your created agent will be visible on the left side of the dashboard as shown below. Identity Step 6 ### Step 7 Now your agent is ready. Now you have to decide whether the agent is a **Chat Agent** or **Voice Agent**. After selecting the type of agent, you have to fill in other details in next steps. Identity Step 7 Now the step of Identity is completed, now you have to move to next parts **Persona**, **Voice Profile**, **Advanced**, **Analysis**, and **Integration**. Let's go through each part step by step. *** ## Persona A "Persona" typically refers to a customizable AI agent or system designed for specialized tasks such as handling conversations, automating support, or performing workflow actions. In this part, you can provide how an AI agent starts the conversation, moving further how it resolves the queries of your customer with a provided system prompt. ### Step 1 - Greeting Message Enter **Greeting Message**. This is the first message which your AI identity says to your client. For example: > "Hello! How can I assist you today?" Persona Step 1 ### Step 2 - System Prompt Provide **System Prompt** which defines the behavior of AI. This part will contain Identity, Style (How your AI Identity behaves), Response Guidelines (How your AI Identity gives response to the client), Tasks and Roles (what roles will be completed by your AI Identity). You have to give clear instructions so that your identity will provide exact and proper information to the client. Persona Step 2 ### Step 3 - Tone and Personality Select the Tone and Personality of your AI Identity. You have four options: **Professional**, **Friendly**, **Casual**, and **Empathetic**. Select according to your business requirements. Persona Step 3 ### Step 4 - Knowledge Base (Optional) Sometimes FAQs are more and not possible to give all information in System Prompt. You can create your own Knowledge Base and connect it with Voice Agent from the dashboard only. Persona Step 4 #### How to Create a Knowledge Base? 1. On the Dashboard, you find the **Knowledge Base** option at the left corner shown below. Knowledge Base Step 1 2. Once you click on the symbol shown above, you will be redirected to the below page. On this page, click on the **Add** button to create a new Knowledge Base. Knowledge Base Step 2 3. Once you click on the **Add** button, you will be redirected to the below page where you can fill the required information to create a new Knowledge Base. Knowledge Base Step 3 You have to fill in the **Name**, **Type of content**, **Description**, and **Visibility** of the knowledge base. **Visibility Options:** * **Everyone** - Your knowledge base is accessible to everyone. * **Shared** - Your knowledge base is only accessible to shared mail ids. * **Private** - Your knowledge base is accessible to you only. 4. Once you fill in all the details, click on the **Next** button and you will be redirected to the page shown below **Add Schema Fields**. Knowledge Base Step 4 Here you can select the necessary fields which are required for your knowledge base. For others you can deselect them. You can also add new fields if needed with the help of **Add Field** option. 5. When you click on **Next**, you will be redirected to the below page. Knowledge Base Step 5 In the above page, you have to add the file that contains required information about your organization or product so that it must be accessible by your AI Voice Agent. After uploading the file, click on the **Save** button. 6. Once you click on the **Save** button, your personal Knowledge Base is created and can be used with your AI Voice Agent to access FAQs or other information. Knowledge Base Step 6 ### Step 5 - Select Model In the next step, you have to select the Model for your Voice AI agent. There are two options to select: * **AI Provider** - You have to select the provider from the given options in the drop down list (OpenAI, Groq, Google, Azure, etc.). Persona Step 5-1 * **AI Model** - You have to select the model from the given options in the drop down list (gpt-3.5-turbo, gpt-4o, etc.). Persona Step 5-2 ### Step 6 - Temperature It is used to adjust the latency of the responses. It is used to control the randomness of AI as well as to adjust how creative the response of AI will be. In simple terms, after what time an AI voice agent gives a response to your question. The recommended value for this parameter is **0.5**. Persona Step 6 ### Step 7 - Max Tokens It represents the maximum token in output as a response for each question you asked from the AI Voice Agent. It would not be more than the given number. The preferred number is **250**. Persona Step 7 ### Step 8 After filling in all the required details, click on **Save** button and move to next part which is **Voice Profile**. *** ## Voice Profile A voice profile is a set of settings that define how an AI or virtual assistant sounds during conversations. It includes choices like the voice's gender, accent, tone, speed, and emotion, allowing businesses to create a natural and consistent speaking style that matches their brand or use case. Voice profiles help make automated calls or chat interactions more engaging and personalized for users. ### Step 1 - Voice Profile Selection First option is Voice Profile. Click on the **Select** button to add the Voice Profile from the given choices. Voice Profile Step 1-1 Voice Profile Step 1-2 Once you select the Voice Profile from the given options, then all other fields will get automatically selected on the basis of the selected agent. ### Step 2 - Transcriber This part has three parameters: * **Transcription Provider** - Service which converts speech to text. * **Transcription Model** - Model which is used to process the transcription. * **Language for Transcription** - Language for speech recognition. Voice Profile Step 2 ### Step 3 - Voice This part has three parameters: * **Voice Provider** - Voice service provider who provides voice to your Voice Agent. * **Voice Model** - The model which is used to process the audio. * **Synthesized Voice** - The name of the voice which is used for text-to-speech. Voice Profile Step 3 ### Step 4 - Telephony This is where you can add the number to which you want to attach your Voice AI Agent. Voice Profile Step 4 ### Step 5 - Config (Optional) This has two fields: **Config Key** and **Config Value**. Voice Profile Step 5-1 Voice Profile Step 5-2 After filling in all the details, click on the **Save** button and move to the next part **Advanced**. *** ## Advanced This is the advanced feature with the help of which you can set up automatic calls. When you set up this feature, your Voice AI agent will automatically call on the provided number at a given time. ### Step 1 - Auto Reachout The first tab is **Auto Reachout**. This tab has following parameters to set up: * **Enable Followup** - This allows the assistant to schedule a follow up with the user automatically. * **Enable Callback** - This allows the assistant to initiate a callback if the call is missed or dropped. * **Handover Number** - This is the number where calls will be forwarded if human handover is triggered. * **Calling Hours** - Define when calls can be placed automatically with flexible scheduling rules. You can set up the time according to flexibility. Advanced Step 1-1 Advanced Step 1-2 ### Step 2 - Stop Speaking Plan The next tab is **Stop Speaking Plan**. This tab has below parameters: * **Number of Words** - This is the number of words that the customer has to say before the assistant will stop talking. * **Voice Seconds** - This is the seconds a customer has to speak before the assistant stops talking. * **Back Off Seconds** - This is the seconds to wait before the assistant will start talking after being interrupted. Advanced Step 2 After filling in all the details, click on the **Save** button and move to the next tab **Analysis**. Advanced Step 3 *** ## Analysis This tab is used to analyse the success of the call logs. This tab has the following parameters: ### Step 1 - Summary This feature is used to provide the prompt used to summarize the call. The output will be stored in `calls.analysis.summary`. You can also find the summary in the **Calls Log** page. This section helps you to derive and summarize the "Summary" of the call according to your business requirements if you need to make any changes in the summary of the call. Analysis Step 1 ### Step 2 - Success Evaluation Evaluate if your call was successful. You can use **Rubric** standalone or in combination with **Success Evaluation Prompt**. If both are provided, they are concatenated into appropriate instructions. Analysis Step 2-1 In the above you can set one Evaluation criteria on the basis of which you can decide whether the call is successful or not. You can set up the Prompt for that. For example, suppose you are a real estate company and your success criteria is "If a customer fixed the site visit" then you consider the call is successful. On the basis of the given prompt, you can set the success evaluation rubric from the selected rubrics according to your understanding. Analysis Step 2-2 ### Step 3 - Structured Data Extract structured data from call conversation. You can use **Data Schema** standalone or in combination with **Structured Data Prompt**. If both are provided, they are concatenated into appropriate instructions. Analysis Step 3-1 Structured data will help you to extract some basic information which is needed to decide whether the called person is interested in your product or service. For example, your agent is related to an educational institution. You have called the parent to provide information about the courses you provide. The basic details you need are the name of the student, grade of the student, etc. In the prompt you can write "Put the child name in the Name tag." and "Put the grade of the child in the Grade tag". Now you have to add the same properties by clicking on the **Add Property** and the name of the properties are case sensitive. Use the same case which you have used in the prompt. Analysis Step 3-2 According to the Tag, click on the property. Suppose you have to make a "Name" tag then select "Text". Analysis Step 3-3 The structured data helps you to extract the exact information of the call and you will be able to analyze the call in a perfect manner. After filling in all the details, click on the **Save** button and move to the next tab **Integration**. Analysis Step 3-4 *** ## Integration Webhook integration is the process of using webhooks to enable real-time communication between web applications, where one application sends data to another as an event occurs. To enable the webhook integration, you have to click on **Enable Webhook** as **Yes**. **Webhook URL** is the endpoint which will be provided by the user. Integration Step 1 You can also add some headers while doing this integration if needed. Webhook headers are the key-value pairs for identification, authentication, and context, telling the receiver who sent it, how to process data, and event details. Integration Step 2 ### Common Webhook Headers **Common and Standard Headers:** * **Content-Type** - Describes the format of the data (e.g., `application/json`, `application/x-www-form-urlencoded`). * **User-Agent** - Identifies the client sending the request (e.g., `GitLab/15.5.0`). * **Content-Length** - Size of the request body in bytes. **Security and Authentication Headers:** * **Authorization** - For bearer tokens or basic auth (e.g., `Bearer `). * **X-Hub-Signature / X-Hub-Signature-256** - HMAC signature to verify the request authenticity (GitHub, etc.). * **X-Shopify-Hmac-Sha256** - The signature of Shopify to verify the delivery. * **Idempotency-Key** - Ensures a request is processed only once, even with retries. **Platform-Specific Headers (Examples):** * **X-GitHub-Event** - Type of event (e.g., `push`, `pull_request`). * **X-Shopify-Topic** - The event topic (e.g., `products/create`). * **X-Gitlab-Event** - GitLab event type (e.g., `Push Hook`). * **X-Contentful-Topic** - Event topic in Contentful. After entering all the required information, click on **Save**. At last click on the **Publish** button at the right top corner and the agent is ready to use. *** **Your AI Agent is now ready to use!** # Analytics and Reporting Source: https://docs.unpod.ai/platform/dev-platform/analytics-and-reporting Monitor performance and generate reports. ## Getting Started Analytics and Reporting refers to the tools and dashboards that track, measure, and visualize the activity and performance of a Space or AI agent. This includes metrics like user interactions, engagement, call outcomes, document usage, and overall agent effectiveness, helping teams make data-driven decisions and optimize workflows. Unpod dashboard provides you with two options to see the reports and analyze the performance of your business. Both the options are available in Space View. *** ## Call Logs This option is available in the Space View. You can access it under the three dots at the top left corner. If you want to download all the details of the calls executed on the space in the form of CSV, you can download it from here.The screenshot is given below: Call Logs Screenshot When you click on Call Logs, it will redirect you to the page where you have to fill in the following details: * **Date range** - Starting and ending date within which you need the report. * **Run id** - Wherever you initiate a call, the dashboard will consider this as a task and create a run id for the same. You can choose the run id for which you want the report. * **Agent** - If you have multiple agents for your organization, then select the agent for which you want the report. * **User** - If you want the report for a particular user, you can select the name of the user. Once you enter all the details, click on Download to get the CSV file. CSV File Screenshot The CSV file will look like below attached screenshot: CSV File Example Screenshot The CSV file contains structured data which you have mentioned while setting up the Analysis settings of the Agent. *** ## Analytics This option is available in Space View. You can access this option from the left side bar of the dashboard. The Analytics tab shows insights and performance metrics related to activity within the Space, including engagement, usage trends, and key statistics. It helps users track progress and measure impact across discussions, calls, and content.The screenshot is attached below: Analytics Tab Screenshot Once you click on Analytics tab, you will be redirected to the analytics dashboard. Analytics Dashboard Screenshot In the above dashboard, you will able to see below details: * **Total Calls** - Number of total calls initiated on a particular space. * **Interested Calls** - Number of people who are interested in the given information. * **Not Connected** - Number of calls who are not connected due to some reasons. * **Avg. Success Score** - The success rate according to the number of connected calls. You will able to see the Call Status breakdown in graphical representation as shown below: Call Status Breakdown Screenshot Calls Analytics Overview is also visible as graphical representation showing detailed metrics and trends. Calls Analytics Overview Screenshot # Attach a Number to an Agent Source: https://docs.unpod.ai/platform/dev-platform/attach-number-to-agent Map your phone numbers to a voice agent over the platform telephony plane — the primary external flow. ## Overview The platform telephony plane (`client.telephony.*`) maps your phone numbers to voice **agents** on backend-core's `/api/v2/platform/telephony/*` surface. The primary flow is **attach a number to an agent** — what telephony calls the **Leg-B** termination (Unpod's SuperSBC routes the inbound call through to your agent on LiveKit). You **list** your org's numbers, then **attach** one or many to an agent. The underlying voice bridge is resolved for you (hidden) — you work with numbers and agents, not bridges. This plane (`client.telephony.*`) is distinct from the Speech Stack management plane at [`client.numbers`](/speech-stack/numbers) / `client.trunks`. The telephony plane is `Org-Handle`-scoped and requires proxy/JWT auth — it is **not** reachable in direct/Bearer mode. *** ## Concepts | Concept | What it is | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Leg B (agent attach)** | SuperSBC → agent/LiveKit. The **primary** flow — `numbers.attach(..., agent_id=...)`. | | **Leg A (BYO carrier)** | Your own SIP carrier → SuperSBC. The carrier leg is the hardcoded SuperSBC default today; bringing your own carrier (`client.telephony.trunks.*`) is **future/beta**. | | **`agent_id`** | The agent handle a number routes to. **Optional** — attach now, bind an agent later. | | **Bridge** | The voice bridge a number rides on. **Auto-resolved and hidden** — one number maps to one bridge. | *** ## Authenticate The telephony plane is org-scoped. Authenticate with a JWT and your org handle: ```python theme={null} import asyncio from unpod import AsyncClient, JWTAuth async def main(): async with AsyncClient(auth=JWTAuth("", org_handle="acme")) as client: numbers = await client.telephony.numbers.list() for n in numbers: print(n.number, n.status) asyncio.run(main()) ``` *** ## List Numbers ```python theme={null} numbers = await client.telephony.numbers.list() ``` ### Number fields Every verb on this plane takes the **E.164 number** — the internal id is not exposed. | Field | Type | Description | | -------- | ------------- | ---------------------------------------------------- | | `number` | `str \| None` | E.164 format, e.g. `+14155550100` | | `status` | `str \| None` | `not_assigned` (attachable), `assigned`, or `closed` | *** ## Attach Numbers to an Agent Map one or more numbers to an agent **by E.164 number** (not id — Django resolves it). The bridge is resolved automatically; each number reports its own outcome (partial-success), so one bad number never fails the rest. ```python theme={null} import asyncio from unpod import AsyncClient, JWTAuth async def main(): async with AsyncClient(auth=JWTAuth("", org_handle="acme")) as client: nums = await client.telephony.numbers.list() res = await client.telephony.numbers.attach( [n.number for n in nums[:2]], agent_id="asst_sales", ) print(res.agent_id, res.message) for r in res.numbers: print(r.number, r.connection_state, "ok" if r.ok else r.error) asyncio.run(main()) ``` ### Result fields | Field | Type | Description | | ---------- | -------------------- | --------------------------------------------------------- | | `agent_id` | `str \| None` | The agent the numbers were bound to (echoes the request). | | `numbers` | `list[NumberResult]` | Per-number outcome (see below). | | `message` | `str \| None` | Human-readable summary. | Each `NumberResult`: | Field | Type | Description | | ------------------ | ------------- | ------------------------------------------------------ | | `number_id` | `int` | The number's ID. | | `number` | `str \| None` | E.164, on success. | | `connection_state` | `str \| None` | Lifecycle state (e.g. `NOT_LINKED`, `PENDING_VERIFY`). | | `agent_id` | `str \| None` | The bound agent, on success. | | `ok` | `bool` | `True` if this number was attached. | | `error` | `str \| None` | Why it failed, when `ok` is `False`. | The agent attach returns the per-number **lifecycle** — there is no carrier `origin_endpoint`. That belongs to the BYO-carrier (Leg-A) path below. ### `agent_id` is optional Omit `agent_id` to wire a number for agent use without binding an agent yet — bind one later by calling `attach` again with the same number and an `agent_id`: ```python theme={null} # provision now, bind later await client.telephony.numbers.attach(["+14155550100"]) # ...later await client.telephony.numbers.attach(["+14155550100"], agent_id="asst_sales") ``` ### Attach to a pipe instead of an agent Pass `attach_type="pipeline"` with a `pipe_id` to terminate the number on a Speech Pipe rather than an agent. `attach_type` defaults to `"agent"`. ```python theme={null} await client.telephony.numbers.attach( ["+14155550100"], attach_type="pipeline", pipe_id="pipe_xyz", ) ``` *** ## Detach Numbers Release numbers by E.164 number — the inverse of `attach`. The termination, agent, and pipe are read from the stored record. The number is **released, not deleted**, so it stays available for a later re-attach. ```python theme={null} res = await client.telephony.numbers.detach(["+14155550100"]) for r in res.numbers: print(r.number, "released" if r.ok else r.error) ``` *** ## Check the Lifecycle ```python theme={null} overview = await client.telephony.overview() for row in overview: print(row.number, row.agent_id, row.connection_state, row.in_sync) ``` *** ## Bring Your Own Carrier (Leg A) — Future / Beta Bringing your own SIP carrier (`client.telephony.trunks.*`) wires the **Leg-A** carrier leg. Today that leg is the hardcoded SuperSBC default, so this surface is **future/beta** — prefer the agent attach above for production. When enabled, the flow is: create a trunk, attach numbers, and point your carrier at the returned origin endpoint. ```python theme={null} # future / beta — BYO SIP carrier (Leg A) trunk = await client.telephony.trunks.create( name="My Carrier", sip_url="sip:carrier.net", username="u", password="p", source_ips=["203.0.113.10"], ) res = await client.telephony.trunks.attach_numbers(trunk.id, number_ids=[1, 2]) print(res.origin_endpoint.ingress) # point your carrier here ``` *** ## Common Patterns ### Attach the first available number to an agent ```python theme={null} import asyncio from unpod import AsyncClient, JWTAuth async def attach_first(agent_id: str) -> None: async with AsyncClient(auth=JWTAuth("", org_handle="acme")) as client: nums = await client.telephony.numbers.list() if not nums: raise RuntimeError("No numbers available") res = await client.telephony.numbers.attach([nums[0].number], agent_id=agent_id) row = res.numbers[0] print("attached" if row.ok else f"failed: {row.error}", nums[0].number) asyncio.run(attach_first("asst_sales")) ``` ### Attach many numbers to one agent ```python theme={null} import asyncio from unpod import AsyncClient, JWTAuth async def attach_all(numbers: list[str], agent_id: str) -> None: async with AsyncClient(auth=JWTAuth("", org_handle="acme")) as client: res = await client.telephony.numbers.attach(numbers, agent_id=agent_id) ok = [r.number for r in res.numbers if r.ok] bad = [(r.number_id, r.error) for r in res.numbers if not r.ok] print("attached:", ok) if bad: print("failed:", bad) asyncio.run(attach_all(["+14155550100", "+14155550101"], "asst_sales")) ``` *** ## Next Steps Build the agent your numbers route to. Acquire and manage numbers from the dashboard. # Introduction Source: https://docs.unpod.ai/platform/dev-platform/introduction Getting started with Unpod Dev - The AI voice agent your business can trust ## What Is Unpod Dev Unpod Dev is a platform for developers which acts as a building block for communication that has four core components, that is, Numbers, Providers, Bridges and Agents. Start with a number, select a provider or channel, route traffic through a bridge, and optionally add a voice agent to automate conversations. ## Unpod Developer Platform **URL:** [https://unpod.ai/](https://unpod.ai/) **Purpose:** This portal is meant for developers, integrators, and technical teams. Contains developer tools, API access, integrations, telephony configuration, and testing utilities. ## How Unpod Dev Works? With Unpod, you can setup three basic components: ### Setup Space A dedicated space that helps to manage your emails, contacts, and documents. ### Setup Agent A custom AI agent for all your needs like customer support and sales support. ### Setup Telephony Add a number, link a voice infra provider, complete KYC, and start calling. ## Why Choose Unpod Dev? * Flexible - Run simple flows (number + provider) or advanced omni-channel workflows * Reliable - 99.99% uptime with automatic failover * Secure - Enterprise-grade security and compliance * Global Reach - Communicate with users worldwide ## Key Capabilities * Real time communication - Natural voice conversations. * Omni Channel Support - Voice, WhatsApp, and Email * AI Driven Automation - Context aware responses and workflow automation * Integration ready - Connect to CRMs, ERPs, and other business tools * Flexible routing - Use numbers, providers, and bridges to control traffic # Load Testing Source: https://docs.unpod.ai/platform/dev-platform/load-testing Perform load testing and stress testing. ## Getting Started Load testing helps ensure your Unpod applications can handle concurrent user traffic and maintain optimal performance under various load conditions. *** ## Performance Targets and SLAs The platform guarantees 99.99% uptime with automatic failover. SLA documentation ### Metric Commitment | Metric | Commitment | Credit Policy | | ----------------------------- | ------------------------------- | -------------------------- | | Uptime SLA | 99.90% available | 0.5% credit per 0.1% below | | End-to-End Latency p99 | less than 1500ms | Included in E2E | | WebApp Service Latency | less than 10ms internal routing | Included in E2E | | Vector Store Query p99 | less than 50ms | Included in E2E | | MongoDB Write Fire and Forget | less than 40ms | Included in E2E | | Data Purge Verification | On-demand audit | Included Enterprise | ### Baseline Performance Metrics The following metrics represent measured performance under optimal baseline conditions single session, warm cache, optimal network: | Component | Measured p50 | Measured p95 | | ------------------------- | ------------ | ------------ | | Platform Orchestration | 8ms | 12ms | | Speech-to-Text STT | 0.5s | 0.7s | | LLM Inference | 0.8s | 1.2s | | Text-to-Speech TTS | 0.3s | 0.5s | | End-to-End Voice Pipeline | 1.6s | 2.4s | ### Concurrent Load Test Results Platform stability validated under concurrent session load: | Test Scenario | Concurrency | Success Rate | Avg Latency | | ----------------------- | ----------- | ------------ | ----------- | | Baseline Single Session | 1 | 100% | 1.6s | | Low Concurrency | 5 | 100% | 1.65s | | Medium Concurrency | 10 | 100% | 1.7s | | High Concurrency | 15 | 100% | 1.7s | ### Infrastructure Robustness | Capability | Status | | ------------------ | --------------------------------- | | Auto-scaling | Horizontal pod scaling enabled | | Failover | Multi-region redundancy | | Connection Pooling | Optimized for concurrent sessions | | Rate Limiting | Per-tenant throttling | | Observability | Real-time latency monitoring | | Data Residency | India region available | *** ## Scalability Architecture * Horizontal Scaling: Native HPA Horizontal Pod Autoscaler for all stateless components * GPU Node Affinity: Dedicated GPU pools with NVIDIA A10G L4 for inference workloads * Regional Infrastructure: Automatic routing through worldwide infrastructure for optimal latency * Database Scaling: Postgres read replicas, MongoDB ReplicaSet with automatic failover * SaaS Auto-scaling: Instant autoscale vs manual capacity planning for self-hosted *** ## Latency Optimization Techniques * Streaming STT TTS: Real-time processing without full-file buffering * Speculative Decoding: Parallel token generation for faster LLM responses * Same Availability Zone: Co-located services to minimize network latency * gRPC WebSocket: Low-overhead protocols for inter-service communication *** ## Notes * End-to-End latency includes external service providers STT, LLM, TTS which contribute to variability under load. * Platform orchestration layer maintains less than 15ms latency regardless of concurrent load. * Performance optimizations for high-concurrency scenarios are actively being deployed. * Custom SLA tiers available for enterprise customers with dedicated infrastructure. # Login Source: https://docs.unpod.ai/platform/dev-platform/login How to login to the Dev Dashboard and set up your voice AI. #### Step 1: Visit Unpod Dev Website Open [https://unpod.ai/](https://unpod.ai/) Unpod Dev Portal #### Step 2: Click on Sign IN option. Now you can login on the [dashboard](https://unpod.ai) and access the dashboard completely. Sign In Screenshot #### Step 3: Complete your Profile When you login for the first time, you will be redirected to the below page to create a hub for your organization. Dashboard Login Screenshot There are four necessary fields which you have to fill: * **Enter Hub Name** - Choose the name of your organization or choose any name that best suits your requirements and organization. * **Domain Name** - Your personal domain name just like gmail.com, yahoo.co, etc. You can choose the domain name from the mail id you are using or make the organization’s name as the domain name like maths.com, music.com, etc. * **Privacy Type** - There are two options - **Public** and **Private**. If you choose public then your hub will be visible to everyone whereas if you choose private then your hub is only visible to people inside your domain. * **Type of Account** - You can choose the type of account according to your requirement from the given five options: Personal\ Startups and Organisations\ Teachers & Educators\ Artists & Creators\ Engineers and Researcher After completion of all steps, you will be redirected to your organisation’s hub. #### Step 4: The hub will look like below Create Hub Screenshot # Numbers Source: https://docs.unpod.ai/platform/dev-platform/numbers Your Voice, Your Number, Any Region. ## Numbers: Your Voice, Your Number, Any Region In Unpod, numbers are virtual phone lines that let your business make and receive calls or messages from anywhere in the world, without using traditional phone lines. Each number can connect to providers, bridges, and agents, which helps create flexible and smart communication. ### Assign Numbers to Bridges #### Step 1 Select the Bridge to which you want to link the number. In the image below, QuickMaths is selected. Number Step 1 #### Step 2 Now click on **Select Available Number** to take the number. When you click on "Select Available Number", it will redirect you to the page below from where you can first upgrade the number and then select that number to integrate on your dashboard. Number Step 2 #### Step 3 Once you click on the **Upgrade** tab you will be redirected to the below page from where you can upgrade the plan or choose the plan to get access to the number. Number Step 3 #### Step 4 When you click on the **Upgrade or View Plans** tab, you will see different plans as well as an option of **Add Billing Info**. Before selecting the plan, you have to fill in your complete details. Number Step 4-1 Number Step 4-2 #### Step 5 After filling the details and selecting the plan, return to the first page and again click on **Select Available Number**. Select the number for your business and click on the **Submit** button. Number Step 5 #### Step 6 After completing all the necessary steps, the number will get added to your Bridge. Number Step 6 *** # Providers Source: https://docs.unpod.ai/platform/dev-platform/provider Connect telecom and SIP providers to Unpod. ## SIP Trunking: Powering Seamless Global Connections SIP trunking replaces traditional phone lines with a virtual connection over the internet, enabling your business to make and receive calls through a broadband connection. It links your internal phone system (PBX or VoIP) to a SIP provider, which then routes calls to the regular phone network. This setup makes your communication system simpler and usually more affordable. *** ## Supported SIP Providers: Bridging You to the World Beyond We have the following providers available for initiating both inbound and outbound calls: * **LiveKit** - Enables real time audio and video calls over the internet using virtual members. * **Vapi** - Provides cloud telephony API for initiating and receiving phone calls programmatically. * **Daily** - Powers multi-party voice and video communication with web-based integrations. * **Twilio** - Offers global programmable voice to connect calls across any device. *** ## Setup Process As you have already selected the number, now you have to configure the number with the available provider. Currently Unpod is providing two options. Let's configure the number with both the providers step by step. *** ### Configure the Number with LiveKit #### Step 1 You already have added the number in your Bridge. Now click on **Configure** to activate the SIP trunking. LiveKit Step 1 #### Step 2 When you click on the **Configure** button, you will be redirected to the below page from where you can choose any SIP provider to enhance the workflow of your organization. LiveKit Step 2 #### Step 3 Now you have various SIP providers with the help of which you can configure the numbers. Let's choose LiveKit for the first selected number. To do this, click on the **Configure** button in front of LiveKit. LiveKit Step 3 #### Step 4 Once you click on the **Configure** button, it will redirect you to the page below where you have to fill in the different details from your LiveKit ID. The details which are mandatory to fill are: * **Name** - You can give any name as per your product or name of the organization. * **API Key** - An API key for LiveKit is a unique code provided by LiveKit that allows your application to securely access and use LiveKit's communication services. * **API Secret** - It acts like a password that ensures only authorized users and applications can access and use the API. You will get this from your LiveKit dashboard. * **Base URL** - A Base URL is the main web address or starting point for accessing an API. * **SIP URL** - It looks similar to an email address (e.g., `sip:username@domain.com`) and is used to identify users or devices for making and receiving VoIP calls over the internet. LiveKit Step 4 #### Step 5 You can access all the above mentioned fields from your LiveKit dashboard except Name. 1. **Access API Key and API Secret:** Go to [https://cloud.livekit.io/](https://cloud.livekit.io/) and then go to **Settings > API Keys** under your project. LiveKit Step 5-1 Here click on Description section to get all the details which are required related with the API keys. LiveKit Step 5-1.1 2. **Access Base URL and SIP URL:** Go to [https://cloud.livekit.io/](https://cloud.livekit.io/) and then go to **Settings > Project**. LiveKit Step 5-2.1 Here Project URL is base URL and SIP URL is mentioned. You can copy it from here and paste on the portal. #### Step 6 After filling all the mentioned fields, click on the **Verify and Configure** button. You can now see Active Providers also on the first page. LiveKit Step 6 #### Step 7 Now click on **Select** in front of QuickMaths(LiveKit) under Active Providers to connect it with your selected number. LiveKit Step 7 #### Step 8 Once you click on the above button, your SIP trunk is activated for use. LiveKit Step 8 #### Step 9 Now as you have activated the SIP trunk, you can configure your trunk for use. To configure, click on **Settings**. LiveKit Step 9 #### Step 10 When you click on settings, you will be redirected to the below page where you have to fill in the below fields and click on submit. * **Concurrency Channels** - Concurrency channel usually refers to a communication channel that supports multiple interactions or processes happening at the same time without interference. Here you can select any number of channels as per your requirement. * **Enter Agent** - Enter the name of your LiveKit agent to start the Voice calling process. LiveKit Step 10 #### Step 11 SIP trunk and number is ready to use. You can start calling with the help of this number. You can use the number for inbound as well as outbound calls as per your requirement. You can see the activated SIP Trunk in LiveKit also. LiveKit Step 11 *** ### Configure the Number with cloud.livekit.io #### Step 1 On platform cloud.livekit.io, go to **Telephony > SIP Trunks**. LiveKit Platform Step 1 #### Step 2 Click on **Create SIP trunk** at the top right corner. LiveKit Platform Step 2 #### Step 3 Once you click on **Create new trunk**, you will get the option to create **Inbound** or **Outbound** trunk according to organization's requirement. You can select one, fill the required details and create the trunk. #### Step 4 For **Inbound**, you just need to provide the Name of the Trunk and Phone number. IP address is optional; if you want to allow it for particular IP addresses then mention that else leave the field blank or `0.0.0.0/0` and the trunk is valid for all IP addresses. After filling all the details, click on **Create** and another Inbound Trunk gets created. LiveKit Platform Step 4-1 LiveKit Platform Step 4-2 #### Step 5 For **Outbound**, you have to fill: * **Trunk Name** * **IP Address** (SIP address of the provider from where you have taken the number) * Keep the **Transport Type** as Auto * **Numbers** provided by the provider * **Username** and **Password** provided by the provider You can ask your SPOC to provide the IP address, Username, and Password from the provider's end. Once you fill in the complete details, your Outbound trunk will be created. LiveKit Platform Step 5-1 LiveKit Platform Step 5-2 Now as your SIP trunks are created, you can use the number to boost your business by making Inbound/Outbound calls. *** ### Configure the Number with Vapi #### Step 1 You already have added the number in your Bridge. Now click on **Configure** to activate the SIP trunking. Vapi Step 1 #### Step 2 When you click on the **Configure** button, you will be redirected to the below page from where you can choose any SIP provider to enhance the workflow of your organization. Vapi Step 2 #### Step 3 Now you have various SIP providers with the help of which you can configure the numbers. Let's choose Vapi for the next selected number. To do this, click on the **Configure** button in front of Vapi. Vapi Step 3 #### Step 4 Once you click on the **Configure** button, it will redirect you to the page below where you have to fill in the different details from your Vapi ID. The details which are mandatory to fill are: * **Name** - You can give any name as per your product or name of the organization. * **API Key** - An API key for Vapi is a unique code provided by Vapi that allows your application to securely access and use Vapi's communication services. Vapi Step 4 #### Step 5 You can access the API key from the Vapi dashboard. Go to **dashboard.vapi.ai**, click on **API Keys** and copy **TEST API key** and paste on the portal. Vapi Step 5 #### Step 6 After filling all the mentioned fields, click on the **Verify and Configure** button. You can now see Active Providers also on the first page. Vapi Step 6 #### Step 7 Now click on **Select** in front of QuickMaths (Vapi) under Active Providers to connect it with your selected number. Vapi Step 7 #### Step 8 Once you click on the above button, your SIP trunk is activated for use. Vapi Step 8 #### Step 9 Now as you have activated the SIP trunk, you can configure your trunk for use. To configure, click on **Settings**. Vapi Step 9 #### Step 10 When you click on settings, you will be redirected to the below page where you have to fill in the below fields and click on submit. * **Concurrency Channels** - Concurrency channel usually refers to a communication channel that supports multiple interactions or processes happening at the same time without interference. Here you can select any number of channels as per your requirement. * **Enter Agent** - Enter the name of your Vapi agent to start the Voice calling process. Vapi Step 10 #### Step 11 SIP trunk and number is ready to use. You can start calling with the help of this number. You can use the number for inbound as well as outbound calls as per your requirement. Vapi Step 11 *** ## Publish As you have configured all the required numbers, the last step is to **Publish** them. Click on the **Publish** button at the top of the dashboard. You can check on your Vapi Dashboard that your SIP Trunk is created. *** **Your Telephony setup is now complete and ready to use!** # Register Source: https://docs.unpod.ai/platform/dev-platform/register How to get access to the Dev Dashboard and set up your voice AI. ## How to Get Access to the Dev Dashboard? #### Step 1 - Visit Unpod Dev Website Open [https://unpod.ai/](https://unpod.ai/) Unpod Dev Portal #### Step 2 - Click on Request a Call Back option. Fill in the required fields and click on Submit. Request a Call Back Screenshot #### Step 3 - You will get the login credentials on your E-mail. Request Form Screenshot *** # Security and Compliance Source: https://docs.unpod.ai/platform/dev-platform/security-and-compliance Security and compliance documentation ### Getting Started Unpod is built with enterprise-grade security and compliance as foundational principles. Our zero-retention model ensures maximum data privacy while maintaining full functionality for your voice AI operations. *** ## Data Handling Matrix (ZERO Retention Model) | Data Type | Retention | Storage | Notes | | ------------------------ | --------- | -------------- | ------------------------------- | | Voice Audio (Raw) | 0 seconds | Memory only | Wiped on session end | | Transcripts | 0 seconds | Memory only | Never written to disk | | LLM Prompts/Responses | 0 seconds | GPU memory | No fine-tuning on customer data | | Caller PII (phone, name) | 0 seconds | Never captured | Pass-through only | | Anonymized Metrics | 90 days | Our systems | Latency, duration, errors only | | Recordings/Transcripts | 90 days | Our systems | As per compliance requirements | | Billing Records | 7 years | Our systems | Legal/tax compliance | *** ## Security Controls * **In-Transit Encryption**: TLS 1.3 for all API calls, SRTP for voice streams * **Memory Isolation**: Each session in isolated container with dedicated memory space; no cross-tenant leakage * **Secure Wipe**: Memory overwritten with zeros on session termination; containers destroyed, not recycled * **No Model Training**: Customer data never used for training or fine-tuning; contractually guaranteed *** ## Compliance Certifications Built-in compliance with ISO-27001, and telecom regulations. | Certification/Standard | Status | | ---------------------- | ----------------------------------------------------------------------------------------------------------- | | ISO/IEC 27001:2021 | Annual audit; report available under NDA | | GDPR Article 17 | Compliant - Zero retention model inherently supports right to erasure | | RBI Data Localization | India-only processing (Mumbai ap-south-1); no data leaves jurisdiction. Other than explicitly chosen models | *** ## Access Control and SSO SSO integration documentation: * **Role-based access control** for admin console (Admin, Editor, User Roles) * **SSO integration** available (SAML 2.0, OAuth 2.0) * **Audit logging** for all administrative actions * **API key rotation and revocation** capabilities # Telephony Source: https://docs.unpod.ai/platform/dev-platform/telephony Configure and manage calling, SIP, and voice services in Unpod. ## Getting Started Unpod enables businesses to rent Regional phone numbers and seamlessly integrate with Voice Infrastructure Providers and Voice AI agents. You can configure your SIP trunk, connect with providers like **LiveKit**, **Vapi**, etc and meet regulatory compliance for smooth operations. *** ## Bridges: Making Sure Every Call Finds Its Way Home Bridges are known as central controllers that manage every call. They connect phone numbers, service providers, and AI agents to ensure each conversation is directed correctly and smoothly. Bridges help you set up how calls move, assign which service provider will handle each number, connect with AI or phone systems for smarter call handling, and keep all your important documents managed and organized for easy access and proper record-keeping. Think of them as the traffic police guiding all communication traffic efficiently. ### Steps to Create Bridges on Dashboard ### Step 1 Login to the [Dashboard](https://unpod.ai) with the credentials provided by the Unpod technical team. Bridge Step 1 ### Step 2 Once you login, it will redirect you to the dashboard. Bridge Step 2 ### Step 3 Now click on the **Setup Telephony** tab to access the Telephony Bridge. Bridge Step 3 ### Step 4 When you click on Setup Telephony, it will redirect you to the below page from where you will be able to create your first Bridge. Bridge Step 4 ### Step 5 Enter the Bridge title in the required tab. Bridge Step 5 ### Step 6 After entering the suitable title for your first bridge, click on the **Publish** button. Your first Bridge is ready to use. You can use this Bridge to boost your business by integrating any virtual number or your own SIP trunk configuration. Bridge Step 6 ### Step 7 In the same space, if you want to create another bridge then click on **➕** symbol. You will be able to add as many spaces you want to add by following the same process discussed above. Bridge Step 7 *** # Introduction Source: https://docs.unpod.ai/platform/introduction The Unpod Platform: an open-source, AI-native communication platform - use the managed cloud or host it on your own infrastructure. ## The Unpod Platform Unpod is an **open-source, AI-native communication platform** ([github.com/unpod-ai/unpod](https://github.com/unpod-ai/unpod), MIT-licensed) for creating AI agents with dedicated phone numbers. Agents handle incoming calls and messages, filter communications intelligently, and deliver actionable insights - integrated with your existing business tools. You can use it two ways: Sign up at [unpod.ai](https://unpod.ai) and build agents in the browser. Nothing to install, nothing to operate. Run the full stack on your own infrastructure - one Docker Compose command brings up everything with working defaults. The same platform powers both: agents you build in the cloud run identically on-prem, so you can start managed and move in-house (or the reverse) without rebuilding. ## What the platform includes * **AI Voice Agents** - conversational agents powered by LLMs, with customizable personality, knowledge, and tools * **Agent Studio** - visual no-code builder for agent behavior, prompts, and workflows * **Multi-channel** - voice calls, WhatsApp, and email through one agent interface * **Telephony** - dedicated phone numbers with SIP trunking and call routing * **Knowledge Base** - upload documents and data sources for RAG-powered responses * **Multi-tenant workspaces** - organizations, teams, RBAC, and shared Spaces * **Call analytics** - real-time dashboards, conversation logs, performance metrics * **Workflow automation** - trigger scheduling, CRM updates, and notifications from conversations * **Desktop app** - native cross-platform client built with Tauri Under the hood it is a monorepo of a Next.js web app, a Django REST backend, FastAPI microservices, and a real-time voice engine (LiveKit + Pipecat) - see [Architecture](/platform/self-hosting/architecture) for the full picture. ## Getting started (managed cloud) Sign up at [unpod.ai/auth/signup](https://unpod.ai/auth/signup/). You can use your email or Google sign-in. After verifying with OTP, you will be guided to create your first voice identity. A Space is your organization's workspace. During onboarding, you will create one with your business details, domain, and voice profile. Open **AI Studio** and create a voice agent. Configure its identity, persona, voice, and connect it to an AI model (OpenAI, Groq, Google, Azure). Set up telephony by creating a bridge, assigning a phone number, and configuring a voice provider. Click **Publish** and your agent is ready to take calls. All you need is an email account and a modern browser. ## Getting started (self-hosted) ```bash theme={null} git clone https://github.com/unpod-ai/unpod cd unpod docker compose -f docker-compose.simple.yml up -d --build ``` That starts the full stack in containers - frontend on `:3000`, API on `:8000`, services on `:9116` - with working defaults. The [Self-Hosting Quickstart](/platform/self-hosting/quickstart) covers setup options, [Configuration](/platform/self-hosting/configuration) the environment, and [Architecture](/platform/self-hosting/architecture) the moving parts. ## Detailed guides Full signup and onboarding walkthrough. Conversations, calls, people, and analytics. Create and configure your AI voice agent. Upload documents and FAQs for your agent. Writing code instead? The [Speech Stack](/get-started/quickstart) gives you the same calling infrastructure as a [Python SDK](https://github.com/unpod-ai/unpod-python-sdk). # Create Your AI Identity Source: https://docs.unpod.ai/platform/onboarding Onboarding walkthrough: create your voice identity and first agent in the Unpod Platform. ### Step 1 Enter OTP and click on "Verify". You will be redirected to the page "Create your Voice Identity". Step 3 - OTP Verification ### Step 2 First section is "Identity" which is the identity of your business. You can import the details from your business website by using "Quick Import from Website" or you can fill the details manually. When you import it from a website, all your details will be automatically taken by the CRM. The details required are: * **Name** - Your business name. * **Domain Name** - Enter the domain name of your business, like gmail.com, yahoo.co, etc. * **Purpose** - The purpose of your identity. You can choose from business, personal, or service according to the requirements of the business. * **Description** - This sub-section will describe your business, what it does, what services or products it will provide to your business. * **Tags** - Which type of identity you are creating for your business, for example, you want to create an identity who provides sales and leads to your business. After importing the business information from the website, the details will look like: Step 4 - Identity Details Here you have to select the tags which are more relevant to your business, and click on "Continue". ### Step 3 The second section is "Voice Profile". With the help of this section, you can give voice to your business. You can listen to the available voices and choose the best that suits your business and click on "Continue". Step 5 - Voice Profile ### Step 4 The last section is "Launch", it shows the summary of the identity of your business. Step 6 - Launch Summary ### Step 5 Click on "Launch" and you will be redirected to the page of your organization which is the "Space View" of the organization. Step 7 - Space View Now you have complete access to the [dashboard](https://unpod.ai). # Register Source: https://docs.unpod.ai/platform/register Register to get access to the Unpod.ai Dashboard ## How to Get Access to the Unpod.ai Dashboard? ### Step 1 Open [https://unpod.ai/auth/signup/](https://unpod.ai/auth/signup/) Step 1 - Signup Page ### Step 2 Fill in all the details of your organization manually or you can also login with the help of "Sign in with Google" with your existing gmail. After entering all the details, Click on the "Sign Up" button. You will be redirected to the OTP page and get OTP on your registered mail. Step 2 - Organization Details # Platform Architecture Source: https://docs.unpod.ai/platform/self-hosting/architecture How the Unpod Open-Source CPAAS Platform is structured and how its components connect. ## What This Is The Unpod Open-Source CPAAS Platform is the fourth pillar of the Unpod stack. It is a full-stack platform for running, managing, and monitoring AI voice agents at scale. Under the hood it uses `unpod` and `superdialog` to run agents. Those agents connect to the Unpod speech platform, which transcribes caller audio and passes plain text directly into SuperDialog. SuperDialog executes the conversation logic and returns a text reply. Unpod synthesises that into speech and streams it back to the caller. The platform can be self-hosted on any machine or deployed directly on Unpod cloud. *** ## The Four Pillars Unpod four pillars diagram showing communication infrastructure, voice stack, SuperDialog conversation framework, and the open-source CPAAS platform. | Pillar | What it is | | ------------------- | -------------------------------------------------------------------- | | Communication Infra | Phone numbers, SIP, PSTN - calling primitives | | Speech Stack | STT, TTS, VAD, barge-in, endpointing | | SuperDialog | Conversation framework - flow graphs, tools, state | | CPAAS Platform | Open-source dashboard, agent studio, analytics, telephony management | *** ## How the Pieces Connect Unpod voice stack diagram showing user entrypoints, the Unpod managed layer, and your agent connected by audio and text turns. The platform manages the full lifecycle around this loop: provisioning agents, attaching numbers and voice profiles, storing transcripts, surfacing analytics, and dispatching runners via `unpod`. *** ## Monorepo Structure ``` unpod/ ├── apps/ │ ├── web/ # Next.js frontend - dashboard, studio, analytics │ ├── backend-core/ # Django REST API - auth, orgs, RBAC, agents │ ├── api-services/ # FastAPI microservices - search, messaging, tasks │ ├── super/ # Voice engine - orchestrator, dispatch, pipeline │ └── unpod-tauri/ # Desktop app (Tauri 2) ├── libs/ │ └── nextjs/ # Shared React libraries (@unpod/*) ├── infrastructure/ │ └── docker/ # Dockerfiles + service configs └── scripts/ # Setup, migration, utility scripts ``` *** ## Core Services ### Web Frontend (`apps/web/`) Next.js frontend with App Router. Provides the agent studio, space management, knowledge bases, call logs, and analytics dashboard. ```bash theme={null} npx nx dev web # Dev server at port 3000 npx nx build web # Production build ``` ### Backend Core (`apps/backend-core/`) Django 5 REST API. Handles JWT auth, multi-tenant organisations, RBAC, agent configuration, and telephony management. * **Storage:** PostgreSQL (relational), MongoDB (documents), Redis (cache) * **Endpoints:** All under `/api/v1/` ```bash theme={null} cd apps/backend-core python manage.py runserver # API at port 8000 ``` ### API Services (`apps/api-services/`) FastAPI microservices for document store, AI search, messaging, and task management. ```bash theme={null} cd apps/api-services uvicorn main:app --host 0.0.0.0 --port 9116 --reload ``` | Route | Service | Description | | ---------------------- | ------------------ | ------------------------- | | `/api/v1/store` | store\_service | Document store + indexing | | `/api/v1/search` | search\_service | AI-powered search | | `/api/v1/conversation` | messaging\_service | Chat conversations | | `/api/v1/agent` | messaging\_service | Agent management | | `/api/v1/task` | task\_service | Task management | ### Voice Engine (`apps/super/`) Orchestrator and worker dispatch layer. Uses `unpod` to register runners and `superdialog` to execute conversation flows. Connects to the Unpod speech platform for STT/TTS. ```bash theme={null} cd apps/super uv run super_services/orchestration/executors/voice_executor_v3.py start ``` #### Media plane (the `RoomEngine` seam) Every voice session has an infra-level **media session** owned by a backend-agnostic **`RoomEngine`** - the orchestrator never speaks a specific media vendor's vocabulary, so the backend is a swap, not a rewrite. * **Today:** self-hosted **LiveKit OSS** (`LiveKitRoomEngine`) provides the SFU, SIP/PSTN, egress/recording, simulcast, and transfer. An in-process engine backs dev and tests. * **Backend selection:** at dispatch the orchestrator calls `pick_engine(required={sip})`, which skips any engine that lacks the required capabilities (`sip`, `egress`, `simulcast`, `transfer`) or reports unhealthy. * **Self-healing:** `create_room` is idempotent per session, `destroy_room` is a no-op on an already-gone room, and a reaper sweeps orphaned rooms left behind by a dead worker. * **Failure mode:** if no healthy, SIP-capable backend exists, dispatch returns **`503`** and the session is finalized `status=failed`, `end_reason=media_unavailable`. * **Scale path:** a future `mediasoup` engine registers behind the same seam (advertising no `sip`), so `pick_engine` keeps PSTN on LiveKit and routes WebRTC-only traffic to mediasoup - no orchestrator change. *** ## Tech Stack | Layer | Technology | | ------------ | --------------------------------- | | Frontend | Next.js / React / Ant Design | | Backend | Django 5 + DRF / FastAPI | | Voice Engine | unpod + SuperDialog + Pipecat | | Databases | PostgreSQL 16, MongoDB 7, Redis 7 | | Messaging | Kafka, Centrifugo | | Desktop | Tauri 2 | *** ## Deployment Options **Self-hosted** - Run the full stack on your own infrastructure using Docker Compose. See [Self-Hosting Guide](/platform/self-hosting/quickstart). **Unpod Cloud** - Deploy runners and agents directly on Unpod's managed infrastructure. The speech platform, orchestration, and storage are all handled for you. *** ## Next Steps Get the platform running on your machine. Numbers, voice profiles, agents, and the SDK. The conversation framework that runs inside every agent. Full REST API documentation. # Configuration Source: https://docs.unpod.ai/platform/self-hosting/configuration Environment variables, development commands, and Docker setup ## Environment Configuration Copy `.env.example` to `.env` at the repo root. The Docker simple setup passes all variables to containers automatically. For local development, each app reads config from: | App | Config Source | | -------------- | ------------------------------------------------------------- | | `backend-core` | `.env` in its own directory (`DJANGO_READ_DOT_ENV_FILE=True`) | | `api-services` | `.env` from monorepo root via python-dotenv | | `web` | `apps/web/.env.local` (copy from `.env.local.example`) | | `super` | `.env` from monorepo root via python-dotenv | *** ### Frontend (`apps/web/.env.local`) | Variable | Default | Description | | ---------------- | ------------------------------------------- | ------------------------ | | `API_URL` | `http://localhost:8000` | Backend API base URL | | `PRODUCT_ID` | `unpod` | Product identifier | | `IS_DEV_MODE` | `true` | Enable development mode | | `CURRENCY` | `USD` | Default currency | | `LIVEKIT_URL` | `ws://localhost:7880` | LiveKit WebSocket URL | | `CENTRIFUGO_URL` | `ws://localhost:8000/connection/centrifugo` | Centrifugo WebSocket URL | ### Backend Core (`.env`) | Variable | Required | Description | | ------------------- | -------- | ------------------------------------------ | | `DJANGO_SECRET_KEY` | Yes | Django secret key | | `POSTGRES_HOST` | Yes | PostgreSQL host (`localhost`) | | `POSTGRES_PORT` | Yes | PostgreSQL port (`5432`) | | `POSTGRES_DB` | Yes | Database name (`unpod_db`) | | `POSTGRES_USER` | Yes | Database user (`postgres`) | | `POSTGRES_PASSWORD` | Yes | Database password | | `MONGO_DSN` | Yes | MongoDB connection string | | `REDIS_URL` | Yes | Redis URL (`redis://localhost:6379/1`) | | `BASE_URL` | Yes | Backend base URL (`http://localhost:8000`) | | `BASE_FRONTEND_URL` | Yes | Frontend URL (`http://localhost:3000`) | ### Voice AI (`apps/super`) | Variable | Required | Description | | -------------------- | -------- | ------------------------- | | `LIVEKIT_URL` | Yes | LiveKit WebSocket URL | | `LIVEKIT_API_KEY` | Yes | LiveKit API key | | `LIVEKIT_API_SECRET` | Yes | LiveKit API secret | | `OPENAI_API_KEY` | Yes | OpenAI API key | | `ANTHROPIC_API_KEY` | Yes | Anthropic API key | | `DEEPGRAM_API_KEY` | Yes | Deepgram STT API key | | `CARTESIA_API_KEY` | Yes | Cartesia TTS API key | | `PREFECT_API_URL` | Yes | Prefect orchestration URL | ### Optional Variables | Variable | Description | | ------------------------- | --------------------------- | | `AWS_ACCESS_KEY_ID` | S3 storage access key | | `AWS_SECRET_ACCESS_KEY` | S3 storage secret key | | `AWS_STORAGE_BUCKET_NAME` | S3 bucket name | | `SENDGRID_API_KEY` | SendGrid API key for emails | See `.env.example` for the full list. *** ## Development Commands ### Make (uses `docker-compose.simple.yml`) | Command | Description | | ------------------ | ---------------------------------------------- | | `make quick-start` | Full setup: env + deps + docker + db + migrate | | `make dev` | Start frontend + backend dev servers | | `make docker` | Start Docker containers | | `make migrate` | Run Django migrations | | `make stop` | Stop Docker containers | | `make clean` | Stop containers and remove all data | | `make logs` | Tail Docker container logs | | `make superuser` | Create Django superuser | ### NPM | Command | Description | | ---------------------- | --------------------------------- | | `npm run dev` | Start web + backend-core (via NX) | | `npm run dev:frontend` | Frontend only (port 3000) | | `npm run build` | Build frontend | | `npm run test` | Run tests | | `npm run e2e` | E2E tests (Playwright) | | `npm run lint:all` | Lint all projects | | `npm run graph` | View NX dependency graph | *** ## Docker ### Development Setup (Recommended) Uses `docker-compose.simple.yml` - single PostgreSQL instance, all services pre-configured: ```bash theme={null} docker compose -f docker-compose.simple.yml up -d # Start docker compose -f docker-compose.simple.yml logs -f # Logs docker compose -f docker-compose.simple.yml down # Stop docker compose -f docker-compose.simple.yml down -v # Stop + remove data ``` | Container | Port | Service | | -------------------- | ----- | ------------- | | `unpod-postgres` | 5432 | PostgreSQL 16 | | `unpod-mongodb` | 27017 | MongoDB 7 | | `unpod-redis` | 6379 | Redis 7 | | `unpod-centrifugo` | 8100 | Centrifugo v5 | | `unpod-backend-core` | 8000 | Django API | | `unpod-api-services` | 9116 | FastAPI | | `unpod-web` | 3000 | Next.js | ### Full Infrastructure Uses `docker-compose.yml` - separate PostgreSQL per service + Kafka (KRaft). For microservices development: ```bash theme={null} docker compose up -d ``` *** ## Database Commands ```bash theme={null} cd apps/backend-core # Run migrations python manage.py migrate # Create new migrations after model changes python manage.py makemigrations # Seed reference data python manage.py seed_reference_data # Setup scheduled tasks python manage.py setup_schedules ``` *** ## Testing and Quality ```bash theme={null} cd apps/backend-core # Run tests pytest # Run tests with coverage pytest --cov # Type checking mypy unpod # Code formatting black unpod ``` *** ## Next Steps Monorepo structure, tech stack, and services overview. Programmatic access to the Unpod platform. # Quickstart Source: https://docs.unpod.ai/platform/self-hosting/quickstart Get Unpod running on your machine in minutes ## Developer Quickstart Get the Unpod platform running locally and deploy your first voice agent. Choose the setup method that works best for you. ### Prerequisites Before you begin, ensure you have: * **Node.js** v20+ / **npm** v10+ * **Python** 3.11+ (3.10+ for `apps/super`) * **Docker** and **Docker Compose** * **Git** * **uv** (only for `apps/super`) *** ### Option 1: One-Command Setup (Recommended) The fastest way to get everything running: ```bash theme={null} git clone https://github.com/unpod-ai/unpod.git cd unpod make quick-start # Install deps, start Docker, run migrations make dev # Start frontend (port 3000) + backend (port 8000) ``` *** ### Option 2: Manual Setup If you prefer full control over each step: ```bash theme={null} # Install Node.js dependencies git clone https://github.com/unpod-ai/unpod.git cd unpod npm install ``` Create Python venv for backend: ```bash theme={null} python3 -m venv apps/backend-core/.venv source apps/backend-core/.venv/bin/activate pip install -r apps/backend-core/requirements/local.txt ``` Start infrastructure (PostgreSQL, MongoDB, Redis, Centrifugo): ```bash theme={null} docker compose -f docker-compose.simple.yml up -d postgres mongodb redis centrifugo ``` Run migrations and start dev servers: ```bash theme={null} cd apps/backend-core && python manage.py migrate --no-input && cd ../.. npm run dev ``` For detailed environment variable configuration, see the [Configuration](/platform/self-hosting/configuration) page. *** ### Option 3: Docker Only (No Local Dependencies) Starts everything in containers with working defaults: ```bash theme={null} git clone https://github.com/unpod-ai/unpod.git cd unpod docker compose -f docker-compose.simple.yml up -d --build ``` Default admin: `admin@unpod.ai` / `admin123` *** ### Access Points Once running, the following services are available: | Service | URL | | ------------ | ------------------------------------------------------------------------ | | Frontend | [http://localhost:3000](http://localhost:3000) | | Backend API | [http://localhost:8000/api/v1/](http://localhost:8000/api/v1/) | | Admin Panel | [http://localhost:8000/unpod-admin/](http://localhost:8000/unpod-admin/) | | API Services | [http://localhost:9116/docs](http://localhost:9116/docs) | | Centrifugo | [http://localhost:8100](http://localhost:8100) | *** ### Verify It Works 1. Open [http://localhost:3000](http://localhost:3000) in your browser 2. Log in with the default credentials or create a new account 3. Create a voice agent from the AI Studio 4. Assign a phone number and make a test call *** ### Next Steps Environment variables, development commands, and Docker setup. Understand the monorepo structure, tech stack, and how services connect. Configure telephony, agents, and providers via the developer dashboard. Programmatic access to agents, calls, numbers, and more. # Analytics Source: https://docs.unpod.ai/platform/space-view/analytics View analytics and performance metrics for your space. ## Analytics The Analytics tab shows insights and performance metrics related to activity within the Space, including engagement, usage trends, and key statistics. It helps users track progress and measure impact across discussions, calls, and content. Analytics Tab Screenshot # Calls Source: https://docs.unpod.ai/platform/space-view/calls Manage voice calls and call settings in your space. ## Calls Call View is a dedicated interface where you can manage voice communication activities - including live calls, call history, transcripts, summaries, and call related actions - all within the context of that space. This section provides a centralized space to initiate voice calls directly from within the space, view ongoing voice sessions, and manage call settings. Call View Screenshot In the call section, select the contact from the left side bar on which you have to make the call, then click on Call at the top right corner. You will be redirected to the page from where you can select the Voice agent and make the call. Call Selection Screenshot Here, you have two options, first is directly click on Submit that will initiate the call immediately, second is the Schedule option from where you can schedule the call for later. When you click on Schedule, you will be redirected to the schedule page. Schedule Options Screenshot You have three options in the above scenario: * **Now** - This is the default option * **Auto Schedule** - This will take the pre-configured agent hours which we provide while creating the agent * **Custom Schedule** - In this option, you can select the date and time manually At the top right corner of the dashboard, you will be able to see the status of the call - outgoing/inbound or Pending/In Progress/Completed etc. Call Status Screenshot In the call section, you have two more tabs - Overview and Conversation. ### Overview In the overview, you will be able to find the entire details: * **Summary** - Details about what the agent is offering * **Call recording** - Call recording of the entire conversation * **Analytics** - In this section, all parameters and outcomes of the call are shown like what is the interest level of the customer, which agent is used, outcome of the call, cost of the call, who triggered the call, etc * **Next Action** - If any action is required by the client to fulfill the requirements of the customer * **Follow Up** - This will show whether we have to take the follow up from the customer or not. This is the case when we see a customer is interested in a product or service Overview Tab Screenshot ### Conversation In the conversation, chat between the customer and agent is visible. You may take the key points without listening to the whole recording. Conversation Tab Screenshot *** # Conversation Source: https://docs.unpod.ai/platform/space-view/conversation Manage conversations and interactions in your space. ## Conversation This is your go-to-hub for open conversations, quick questions, and lively discussion with the Unpod community. Whether you are seeking help, sharing insights, or just connecting with others, this is the place to talk, collaborate, and stay in the loop. With the help of Ask me anything, you can ask anything. Feel free to: * Ask questions or share ideas * Discuss topics related to Unpod and beyond * Help others with your knowledge and experience * Keep the conversation respectful and inclusive Add Field Screenshot *** # Introduction Source: https://docs.unpod.ai/platform/space-view/introduction Spaces are your personalized work hubs inside Unpod - organized, focused, and tailored so users can work smarter and collaborate better. ## Introduction to Space Mode The interface below represents the Space View. Space View Screenshot The left side bar of the Space View has four sections - Conversation, Calls, People, and Analytics. At the left corner of the [dashboard](https://unpod.ai), you can see three dots from where you will be able to edit the space, copy the Space token, and download the Call Logs. Three Dots Menu Screenshot ## Edit Space When you click on Edit space, you will be able to edit basic information of the space, Table schema which is the predefined fields required while adding the contact, and also able to link the agent. Edit Space Screenshot The main part to edit is Table Schema as the default schema has some fields which are not required or also we need some more fields according to the need of the business. When you click on Table Schema you will get the default fields. Table Schema Screenshot As you can see two fields are mandatory Name and Contact Number which you can not delete from the schema, rest of the fields can be deleted from the schema as per the requirement of the business. There is a check box in front of each field, if the check box is ticked that means the field is compulsory to fill but if there is no tick that means the field is not compulsory to fill. You can also add new fields according to the business requirement by clicking on Add Field at the top right corner. When you click on Add Field, you will get the options of which type of field you want to add like Text, Text Area, File, Date, Time, Date and Time, etc. Conversation Section Screenshot ## Space Token Space Token is a unique access credential used to identify and authorize a user within a specific Space. It is needed to secure access, control permissions, track activity, and ensure that actions (calls, docs, analytics) are correctly linked to the right Space and users. # People Source: https://docs.unpod.ai/platform/space-view/people Manage contacts and people in your space. ## People With the help of this section, you will be able to add contacts to the space of your organization. At the middle of the page click on New Contact. New Contact Screenshot When you click on New Contact, you will be redirected to the page from where you can add the contacts manually or you can attach the CSV in a predefined schema (the fields which are needed for your business). Contact Options Screenshot In the above screenshot, the first option is Import, you can import the CSV from your system in a predefined schema. Second option is Add Manually, when you click on this option you will be redirected to the page where you can fill the required fields. Add Manually Screenshot *** # Advanced Source: https://docs.unpod.ai/platform/studio-view/advance Configure advanced settings and automatic call features. ## Advanced This is the advanced feature with the help of which you can set up automatic calls. When you set up this feature, your Voice AI agent will automatically call on the provided number at a given time. ### Context Settings This setting enables the AI assistant to retain and reference relevant details from past conversations, allowing it to better understand user context over time. By using conversation memory, the assistant can deliver more personalized, consistent, and context-aware responses instead of treating each interaction as isolated. Advanced Settings Screenshot ### Auto Reachout * **Enable Followup** - This allows the assistant to schedule a follow up with the user automatically. * **Enable Callback** - This allows the assistant to initiate a callback if the call is missed or dropped. * **Notify via SMS** - This option sends an SMS notification to the user when the assistant is unable to connect via a call. * **Handover Number** - This is the number where calls will be forwarded if human handover is triggered. Auto Reachout Screenshot ### Calling Hours Define when calls can be placed automatically with flexible scheduling rules. You can set up the time according to flexibility. Calling Hours Screenshot ### Stop Speaking Plan * **Number of Words** - This is the number of words that the customer has to say before the assistant will stop talking. * **Voice Seconds** - This is the seconds a customer has to speak before the assistant stops talking. * **Back Off Seconds** - This is the seconds to wait before the assistant will start talking after being interrupted. Stop Speaking Plan Screenshot After filling in all the details, click on the Save button and move to the next part Analysis. # Analysis Source: https://docs.unpod.ai/platform/studio-view/analysis Analyze call success and extract structured data. ## Analysis This tab is used to analyse the success of the call logs. ### Summary This feature is used to provide the prompt used to summarize the call. The output will be stored in calls.analysis.summary. You can also find the summary in the Calls Log page. This section helps you to derive and summarize the Summary of the call according to your business requirements if you need to make any changes in the summary of the call. Analysis Tab Screenshot ### Success Evaluation Evaluate if your call was successful. You can use Rubric standalone or in combination with Success Evaluation Prompt. If both are provided, they are concatenated into appropriate instructions. Summary Section Screenshot In the above you can set one Evaluation criteria on the basis of which you can decide whether the call is successful or not. You can set up the Prompt for that. For example, suppose you are a real estate company and your success criteria is If a customer fixed the site visit then you consider the call is successful. On the basis of the given prompt, you can set the success evaluation rubric from the selected rubrics according to your understanding. Success Evaluation Screenshot ### Structured Data Extract structured data from call conversation. You can use Data Schema standalone or in combination with Structured Data Prompt. If both are provided, they are concatenated into appropriate instructions. Structured data will help you to extract some basic information which is needed to decide whether the called person is interested in your product or service. Structured Data Screenshot For example, your agent is related to an educational institution. You have called the parent to provide information about the courses you provide. The basic details you need are the name of the student, grade of the student, etc. In the prompt you can write Put the child name in the Name tag. and Put the grade of the child in the Grade tag. Now you have to add the same properties by clicking on the Add Property and the name of the properties are case sensitive. Use the same case which you have used in the prompt. Add Property Screenshot According to the Tag, click on the property. Suppose you have to make a Name tag then select Text. Property Selection Screenshot The structured data helps you to extract the exact information of the call and you will be able to analyze the call in a perfect manner. After filling in all the details, click on the Save button and move to the next tab Integration. # API Key Source: https://docs.unpod.ai/platform/studio-view/api-key Generate and manage API keys for your agent. ## API Key An API key is a secure, unique identifier used to authenticate and authorize an application or agent to access specific APIs and services. You can generate the API key from the given area. API Key Screenshot # Call Logs Source: https://docs.unpod.ai/platform/studio-view/call-logs Download and manage call logs from your space. ## Call Logs If you want to download all the details of the calls executed on the space in the form of CSV, you can download it from here. *** # Dashboard Source: https://docs.unpod.ai/platform/studio-view/dashboard Configure agents and workspaces, all in one place. ### Getting Started Unpod Agents are AI-powered assistants that handle calls, chats, and tasks. With an agent, you can decide how it interacts with users, what knowledge it accesses, and how it communicates over the phone. The interface below represents the Studio View. Agent Configuration Screenshot As you have already created a Voice Agent for your business while getting access to the [dashboard](https://unpod.ai). But you can modify the Agent according to your advanced requirements or you can create a New Agent also by clicking on Create AI Identity. It will redirect you to the Agents page. Agent Configuration Screenshot The above page shows the configuration of already created agent as well as you can create a new agent by clicking on plus symbol at the left corner of the above page. It will again redirect you to the base page which you have encountered in the start while SignUp process. *** Agent Configuration Screenshot *** *** *** *** *** *** *** # Identity Source: https://docs.unpod.ai/platform/studio-view/identity Define your AI agent name, role, and persona. ## Identity Identity defines the AI agent's name, role, and persona that determine how it presents itself and interacts with users. The main sections of the Identity are: * **Description** - A high-level description of the AI assistant - what it does, its domain (e.g., rolling mill machinery), and its primary purpose. This helps users quickly understand the assistant's focus and capabilities. * **Privacy** - It means whether the agent is accessible to everyone or it remains private to only some people. * **Classification** - Mention the keywords that help the users to discover your agent. * **Purpose** - The purpose for which you have created the agent, Business, Personal, and Service. Agent Configuration Screenshot After filling in all the details, click on the Save button and move to the next part Persona. # Integration Source: https://docs.unpod.ai/platform/studio-view/integration Configure webhook integrations and API connections. ## Integration Webhook integration is the process of using webhooks to enable real-time communication between web applications, where one application sends data to another as an event occurs. Webhook Integration Screenshot To enable the webhook integration, you have to click on Enable Webhook as Yes. Webhook URL is the endpoint which will be provided by the user. You can also add some headers while doing this integration if needed. Webhook headers are the key-value pairs for identification, authentication, and context, telling the receiver who sent it, how to process data, and event details. Webhook Headers Screenshot ### Common Webhook Headers **Common and Standard Headers:** * **Content-Type** - Describes the format of the data (e.g., application/json, application/x-www-form-urlencoded). * **User-Agent** - Identifies the client sending the request (e.g., GitLab/15.5.0). * **Content-Length** - Size of the request body in bytes. **Security and Authentication Headers:** * **Authorization** - For bearer tokens or basic auth (e.g., Bearer token). * **X-Hub-Signature / X-Hub-Signature-256** - HMAC signature to verify the request authenticity (GitHub, etc.). * **X-Shopify-Hmac-Sha256** - The signature of Shopify to verify the delivery. * **Idempotency-Key** - Ensures a request is processed only once, even with retries. **Platform-Specific Headers (Examples):** * **X-GitHub-Event** - Type of event (e.g., push, pull\_request). * **X-Shopify-Topic** - The event topic (e.g., products/create). * **X-Gitlab-Event** - GitLab event type (e.g., Push Hook). * **X-Contentful-Topic** - Event topic in Contentful. After entering all the required information, click on Save. At last click on the Publish button at the right top corner and the agent is ready to use. *** # Knowledge Base Source: https://docs.unpod.ai/platform/studio-view/knowledge-base Create and manage knowledge bases for your AI agent. ## Knowledge Base (Optional) Sometimes FAQs are more and not possible to give all information in System Prompt. You can create your own Knowledge Base and connect it with Voice Agent from the dashboard only. Template Selection Screenshot ### How to Create a Knowledge Base? #### Step 1 On the Dashboard, you find the Knowledge Base option at the left corner. Knowledge Base Dashboard Screenshot #### Step 2 Click on the Add button, you will be redirected to the page where you can fill the required information to create a new Knowledge Base. Add Knowledge Base Screenshot You have to fill in the Name, Type of content, Description, and Visibility of the knowledge base. **Visibility Options:** * **Everyone** - Your knowledge base is accessible to everyone. * **Shared** - Your knowledge base is only accessible to shared mail ids. * **Private** - Your knowledge base is accessible to you only. #### Step 3 Once you fill in all the details, click on the Next button and you will be redirected to the page where you can upload the CSV file or you can add more schema Fields. Knowledge Base Upload Screenshot #### Step 4 Once you click on the Save button, your personal Knowledge Base is created and can be used with your AI Voice Agent to access FAQs or other information. Knowledge Base Created Screenshot After filling in all the details, click on the Save button and move to the next part Voice Profile. # Persona Source: https://docs.unpod.ai/platform/studio-view/persona Configure your AI agent personality and behavior. ## Persona A Persona typically refers to a customizable AI agent or system designed for specialized tasks such as handling conversations, automating support, or performing workflow actions. In this part, you can provide how an AI agent starts the conversation, moving further how it resolves the queries of your customer with a provided system prompt. ### Greeting Message Enter a Greeting Message. This is the first message which your AI identity says to your client. For example: "Hello! How can I assist you today?" Conversation Tone Screenshot ### Conversation Tone Select the Tone and Personality of your AI Identity. You have four options: Professional, Friendly, Casual, and Empathetic. Select according to your business requirements. Conversation Tone Screenshot ### Behavior Instructions Provide System Prompt which defines the behavior of AI. This part will contain Identity, Style (How your AI Identity behaves), Response Guidelines (How your AI Identity gives response to the client), Tasks and Roles (what roles will be completed by your AI Identity). You have to give clear instructions so that your identity will provide exact and proper information to the client. Conversation Tone Screenshot In this section, you have two options whether you can provide system prompt manually or you can click on Choose Template, choose the template that best suits your business, and then click on Generate with AI. This will give you a pre-built instructions, you can read it and modify the instructions if needed as per business requirements. Conversation Tone Screenshot # Telephony Source: https://docs.unpod.ai/platform/studio-view/telephony Assign phone numbers and configure calling settings. ## Telephony This section allows you to assign a dedicated phone number to the AI agent, enabling it to handle voice interactions. The selected number is used for making and receiving calls, ensuring users can communicate with the agent through a consistent and identifiable telephony channel. Telephony Screenshot After filling in all the details, click on the Save button and move to the next part Advanced. # Voice Profile Source: https://docs.unpod.ai/platform/studio-view/voice-profile Configure voice settings and audio parameters for your AI agent. ## Voice Profile A voice profile is a set of settings that define how an AI or virtual assistant sounds during conversations. It includes choices like the voice's gender, accent, tone, speed, and emotion, allowing businesses to create a natural and consistent speaking style that matches their brand or use case. Voice profiles help make automated calls or chat interactions more engaging and personalized for users. ### Voice Profile Selection The first option is Voice Profile. The default voice is already visible. Click on the Manage Profiles to change the Voice Profile from the given choices. Voice Profile Selection Screenshot Manage Profiles Screenshot Once you select the Voice Profile from the given options, then all other fields will get automatically selected on the basis of the selected agent. ### Model This part has two parameters: * **AI Provider** - is the service platform that hosts and delivers the AI technology used by the agent. * **AI Model** - is the specific intelligence selected from that provider that defines how the agent understands input, reasons, and responds. Model Configuration Screenshot ### Transcriber This part has three parameters: * **Transcription Provider** - Service which converts speech to text. * **Transcription Model** - Model which is used to process the transcription. * **Language for Transcription** - Language for speech recognition. Transcriber Configuration Screenshot ### Voice This part has three parameters: * **Voice Provider** - Voice service provider who provides voice to your Voice Agent. * **Voice Model** - The model which is used to process the audio. * **Synthesized Voice** - The name of the voice which is used for text-to-speech. Voice Configuration Screenshot ### Temperature It is used to adjust the latency of the responses. It is used to control the randomness of AI as well as to adjust how creative the response of AI will be. In simple terms, after what time an AI voice agent gives a response to your question. The recommended value for this parameter is 0.5. Temperature Settings Screenshot ### Max Tokens It represents the maximum token in output as a response for each question you asked from the AI Voice Agent. It would not be more than the given number. The preferred number is 250. Max Tokens Settings Screenshot ### Config (Optional) This has two fields: Config Key and Config Value. Config Settings Screenshot After filling in all the details, click on the Save button and move to the next part Telephony. # Use Cases Source: https://docs.unpod.ai/platform/use-cases Explore ready-to-deploy Voice AI agents across industries - HR, E-Commerce, Fintech, Education, and Beyond. A warm, sharp HR recruiter agent for screening, schedule, interview, update and onboard candidates at scale. Voice agents will conduct structured interviews, filter based on experience, and guide candidates through the hiring process. **Languages:** English, Hindi A warm onboarding coach for new hires. Provides friendly welcome, explains Day 1 and Week 1 activities, and helps new employees feel comfortable. Conducts personalized guidance calls to warmly onboard users. **Languages:** English, Hindi A COD confirmation agent for e-commerce and logistics. Confirms Cash-on-Delivery orders, reduces RTO, and handles delivery-related queries. A lead qualification agent for inbound and outbound leads. Qualifies leads, answers FAQs, and guides serious prospects to the next step in the sales process. A 24/7 inbound customer support agent that answers FAQs and triages customer issues to the right team. Handles basic support and initial triage before escalation. A cart recovery specialist for e-commerce. Calls customers who left items in their cart, helps them complete orders, answers doubts, and recovers sales warmly. Calls every lead to ask qualifying questions, answer FAQs, and warmly introduce the business. A proactive product announcements partner for existing users. Keeps them engaged about feature upgrades, improvements, and new product launches. A front desk agent to answer every call for clinics, hotels, and offices. Handles scheduling, booking confirmations, and general inquiries professionally. A survey agent for automated NPS, CSAT, feedback, and product experience surveys. Runs short, personalized survey conversations that feel human and respectful. A smart reminder coach for EMIs, collections, form-filling, renewals, and other important due dates. Helps users understand, set, confirm, or update reminders. Lead Qualification of Owner for Broker and asks further details about property buying interest. Helps fintech or BFSIs companies sell their credit cards and other services. Helps fintech or BFSIs companies sell their Loan and other services to customers, based on their queries, also check the approvals and update. Help Education institutions like coaching classes and colleges in offering support and counseling to students. Instant KYC verifications with language detection and secure OTP flows. Loan Recovery, Package, Service renewals, Insurance Renewal. Business can Run Awareness campaign through Voice Agent. Engage Users with bilingual campaign messages. # Chat API Source: https://docs.unpod.ai/playbook/api Call your published playbook over an OpenAI-compatible chat/completions endpoint. Publishing a playbook as an endpoint gives you an OpenAI-compatible API. Any OpenAI SDK works - swap the base URL, the API key, and the `model`. | | | | ------------ | -------------------------------------- | | **Base URL** | `https://inference.unpod.ai` | | **Route** | `POST /v1/chat/completions` | | **Auth** | `Authorization: Bearer ` | **Prerequisites:** a published playbook and an endpoint key. Publish, then open **Deploy as Endpoint → Manage API Keys** - see [Publish & share](/playbook/publish-and-share#deploy-as-endpoint). Three surfaces, three auth stories - do not mix them: | Surface | Host | Auth | | ---------------------------------------------------- | -------------------- | ---------------------------------------------- | | **Chat API** (this page) | `inference.unpod.ai` | `Authorization: Bearer ` | | Python SDK | `api.unpod.ai` | `UNPOD_API_KEY` (`sk_...`), handled by the SDK | | [Platform REST API](/api/get-started/authentication) | `unpod.ai` | `Authorization: Token` + `Org-Handle` | ## Request ### Headers | Name | Required | Value | | --------------- | -------- | ----------------------- | | `Authorization` | Yes | `Bearer ` | | `Content-Type` | Yes | `application/json` | ### Body | Field | Type | Required | Description | | ---------- | ------ | -------- | --------------------------------------------------------------------------------------- | | `model` | string | Yes | Your playbook id, `public:` prefixed - e.g. `public:PB_7ZRMzCA1ojQ9LlcK` | | `messages` | array | Yes | `{role, content}` objects, as in the OpenAI API | | `user` | string | No | A stable session id. Pass the same value across requests and the agent keeps its state. | ## Keep a conversation going Without `user`, each request is independent. With it, the agent remembers the thread - the checkpoint it reached, the slots it filled - across requests. ```bash theme={null} -d '{"model":"public:PB_...","messages":[{"role":"user","content":"hi"}],"user":"sess_abc"}' ``` Use one id per caller or per conversation, not one per process. ## Examples ```bash theme={null} curl -X POST "https://inference.unpod.ai/v1/chat/completions" \ -H "Authorization: Bearer $UNPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "public:PB_7ZRMzCA1ojQ9LlcK", "messages": [{"role": "user", "content": "hi"}], "user": "sess_abc" }' ``` ```python theme={null} import os from openai import OpenAI client = OpenAI( base_url="https://inference.unpod.ai/v1", api_key=os.environ["UNPOD_API_KEY"], ) reply = client.chat.completions.create( model="public:PB_7ZRMzCA1ojQ9LlcK", messages=[{"role": "user", "content": "hi"}], user="sess_abc", ) print(reply.choices[0].message.content) ``` ```javascript theme={null} import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://inference.unpod.ai/v1", apiKey: process.env.UNPOD_API_KEY, }); const reply = await client.chat.completions.create({ model: "public:PB_7ZRMzCA1ojQ9LlcK", messages: [{ role: "user", content: "hi" }], user: "sess_abc", }); console.log(reply.choices[0].message.content); ``` ```go theme={null} cfg := openai.DefaultConfig(os.Getenv("UNPOD_API_KEY")) cfg.BaseURL = "https://inference.unpod.ai/v1" client := openai.NewClientWithConfig(cfg) reply, err := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ Model: "public:PB_7ZRMzCA1ojQ9LlcK", Messages: []openai.ChatCompletionMessage{{Role: "user", Content: "hi"}}, User: "sess_abc", }) ``` Keys carry your account's access. Keep them server-side in a secret manager or environment variable - never in client-side code or a committed file. Rotate immediately if one leaks. ```json 200 theme={null} { "id": "chatcmpl-abc123", "object": "chat.completion", "created": 1677858242, "model": "public:PB_7ZRMzCA1ojQ9LlcK", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "Hi! Welcome to Lumina Spa - I'm Mira, your booking assistant. How can I help you today?" }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30 } } ``` ```json 401 theme={null} { "error": { "message": "Invalid API key", "type": "authentication_error", "code": "invalid_api_key" } } ``` ## Next Deploy the playbook and mint a key. Drop this endpoint into LiveKit, Pipecat, or any chat workflow. # Build an agent playbook Source: https://docs.unpod.ai/playbook/build-an-agent Describe the agent you want in plain English - or drop in a PDF or a URL - and the builder agent writes the playbook for you. Watch it land in the Editor, tweak it, save it. ## The idea You do not start from a blank YAML file. You **talk to a builder agent** in the left-hand chat and it authors the playbook live into the **Editor** tab on the right - refine it in chat or edit it by hand. Full tour of the screen: [The Playground](/playbook/playground). Open the Playground Everything below happens at superdialog.unpod.ai/playground. A guest trial lets you build a playbook before creating an account. ## Step 1 - Describe what you want SuperDialog landing composer: 'Turn a prompt into a conversation agent', a text box reading 'Describe the conversation you want to build - e.g. Confirm the appointment, offer Friday 4pm...', a model selector, and suggestion chips. The first message is your onboarding. Type a one-line description of the agent's job. Be concrete about the **outcome** and any **fallbacks**: > *"Confirm an appointment - offer Friday 4pm, fall back to 5pm."* > > *"Handle a refund request with a polite escalation path."* The builder agent turns that into a first playbook: a persona, an opening line, and the checkpoints needed to reach the outcome. Describe the **goal and the guardrails**, not the exact words. "Collect the order number before offering a refund" is a checkpoint the engine can enforce; scripting every sentence fights the model instead of steering it. Why that works: [Thinking in playbooks](/superdialog/thinking-in-playbooks). ### Start from a document or a URL The composer has a `+` **Attach** button and accepts a pasted link: Upload a PDF, TXT, MD, or DOCX (a returns policy, a product sheet, a script). The text is extracted and **stored in the session**, so the agent can re-read it on any later turn - not injected once and forgotten. Drop an `https://` link in the composer. The page is fetched, extracted, and stored the same way. Great for "build an agent from our FAQ page." Sources persist for the whole session. The builder agent has `list_sources` and `read_source` tools, so you can say "check the returns doc again" three turns later and it will. ## Step 2 - Pick the build model (optional) The composer has a **model selector**. It defaults to a strong model that authors good playbooks out of the box, and each option is tagged for its strength - *best for editing*, *most capable*, *top reasoning*, *fast*: Builder model dropdown listing Claude Sonnet 5 (best for editing), Claude Opus 4.8 (most capable), Claude Sonnet 4.6 (best for editing, selected), GPT-5.5 (top reasoning), Claude Haiku 4.5 (fast), and Gemini 3.5 Flash (beta). Override it only if you have a preference - it controls the *builder*, not the model your finished agent will run on (that is set in [Test by voice](/playbook/test-by-voice)). ## Step 3 - Watch it land in the Editor Switch to the **Editor** tab to see the playbook the agent wrote. This is real, editable YAML: Split view: left panel shows YAML editor with persona, env, journeys, and booking checkpoints including slots for name, service, and date; right panel shows Optimized by Agent with a refined version adding synonyms and advance conditions. Two ways to refine from here: "Add a checkpoint that captures a callback number." The agent edits the YAML and the Editor updates. This is the fastest loop. Type directly in the Editor. Validation runs as you type and flags a malformed checkpoint before you test it. ## Step 4 - Save, import & export The top bar holds the three file actions. The bar also shows a **dirty/draft indicator**, so you always know whether the current buffer is saved. | Action | What it does | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Save** | Persists the current playbook as a **draft**, private to you. Preview auto-saves a dirty buffer before a test call, but save explicitly when you pause. | | **Import** | Load a playbook YAML from a file into the editor - move an agent between accounts, or start from a version you keep in git. | | **Export** | Download the current playbook YAML - back it up, review it, or hand it to a teammate. | Saving keeps the playbook **private to you**. Making it visible to others, generating a shareable link, or deploying it happens later, in [Publish & share](/playbook/publish-and-share) - which offers **Deploy as Voice Agent** (a phone number) and **Deploy as Endpoint** (an API). ## What "good" looks like before you test * A clear **persona** and an **opening line** a caller would find natural. * Checkpoints written as **outcomes** (`done_when: the caller confirmed a time`), not scripts. * The **fallbacks** you mentioned are present (offer 5pm; escalate politely). * Any **slots** you need to capture (order number, callback) have a checkpoint. Outcome-shaped checkpoints and slots in depth: [Thinking in playbooks](/superdialog/thinking-in-playbooks). ## Next step Switch to the Preview tab, press to talk, and confirm the agent behaves like a real call before you harden or ship it. # Run it locally Source: https://docs.unpod.ai/playbook/developer-setup Run the Playground on your own machine, point it at hosted or local speech, and understand its transport. The playground ships inside supervoice; one command builds the UI and serves the agent. ## What you are running The Playground is a Vite + React SPA (`playground/web/`) served by a small Python **harness** that runs the agent in-process. It ships inside the **supervoice** repo, which pins `unpod` and `superdialog` as editable sibling deps, plus bundled example flows and playbooks - so it runs self-contained. Agent flows are authored with **superdialog**; the call runtime is the **unpod** SDK `AgentRunner`. The harness re-implements the WS audio protocol from scratch - there is no `pipecat` / `rtvi` client dependency. ## Prerequisites `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`. Copy `playground/.env.example` to `.env` at the repo root and set it. Something the harness can reach for audio. Resolved in the order below - a fresh clone needs no local supervoice if you use a hosted URL. ### How the speech backend is resolved The harness (`harness/api.py`) picks a backend in this order: 1. **`UNPOD_BASE_URL`** → the hosted `wss://` speech service. For hosted voice with zero local backend: `UNPOD_BASE_URL=api.unpod.ai` plus `UNPOD_API_KEY=`. 2. Otherwise **`ws://127.0.0.1:9000`** → a local `supervoice-dev`. Legacy `SUPERVOICE_URL` still overrides both if set (with a deprecation warning) - prefer `UNPOD_BASE_URL`. The harness boots and serves the UI even with no speech backend reachable; only clicking **Connect** (which opens an audio call) needs one. ## Run it From the `supervoice/` repo root (`cp playground/.env.example .env`, set an LLM key): ```bash theme={null} task pg # builds web/dist, then serves UI + in-process agent on :9100 ``` Or manually: ```bash theme={null} cd playground/web && npm install && npm run build && cd ../.. uv run --extra playground python -m playground.run ``` Open [http://localhost:9100](http://localhost:9100), click **Connect**, allow the mic, and talk. `task pg-docker` - playground on `:9100` + local dev-speech on `:9000`, in containers. Set `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` in `supervoice/.env` first. `task pg-stack` - boots a local dev-speech on `:9000` unless a hosted speech URL is set. ### Frontend hot-reload loop Run the harness on `:9100` and Vite separately on `:5173` (it proxies `/playground/*` to the harness): ```bash theme={null} uv run python -m playground.run # terminal 1 - harness :9100 cd playground/web && npm run dev # terminal 2 - Vite :5173 ``` ## Architecture (M1) Two planes, deliberately separate: ``` Browser (web/) ──audio (WS /ws/audio, protobuf)──► remote supervoice (UNPOD_BASE_URL) │ ▲ │ POST /playground/sessions ┌── harness (api.py) ───────┘ AgentRunner registers │ WS /playground/events ◄──┤ side-channel: session hooks → browser └─────────────────────────────┘ ``` * **Audio** rides the supervoice WS bridge directly (16 kHz mic up, 24 kHz playback down). The wire codec is a small proto3 `Frame` encoder/decoder in `web/src/transport/protobuf.ts`; capture/playback in `web/src/transport/audio.ts`. * **Transcript / current checkpoint** ride the harness's **own** side channel (`/playground/events`), fed by SDK `Session` hooks - *not* the audio bridge. This keeps the transcript accurate even when audio is under load. ## Configuration One host + one credential drive every unpod plane. The harness derives the auth, platform, playbook-API, and hosted-speech bases from `UNPOD_BASE_URL` via the unpod SDK resolver. | Setting | Purpose | | -------------------------------------- | -------------------------------------------------------------------------------------------------- | | `UNPOD_BASE_URL` | The one host every plane is derived from (e.g. `api.unpod.ai`). | | `UNPOD_API_KEY` | Credential for hosted speech + platform APIs. | | `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` | The LLM the agent and builder run on. | | `FLOWS_DIR` / `PLAYBOOKS_DIR` | Point at `../superdialog/examples/*` to drive live superdialog copies instead of the bundled ones. | Default builds bake no client-side auth base - the SPA reaches auth through the harness's same-origin proxy at `/playground/auth`. The deprecated `VITE_API_URL` still overrides this (pointing the SPA straight at a hosted backend) if set explicitly mid-migration. ## Related The brain behind every playbook - engines, the Agent protocol, and embedding. Drop SuperDialog into LiveKit, PipeCat, FastAPI, or the CLI directly. The voice infrastructure that serves live calls in production. The builder-facing walkthrough of the same Playground. # Public & your playbooks Source: https://docs.unpod.ai/playbook/library The playbook gallery has two tabs - Public (agents anyone published, browse and clone) and My Playbook (your private drafts). Open either in the Playground, test it, and make it your own. ## Two tabs, two libraries The playbook gallery is where every agent lives. It has two tabs: Every **public** playbook, from all users. Browse without an account, open one in the Playground, talk to it, and **clone** it to start your own. **Your own** private playbooks and drafts. Requires login. Open one to keep editing, testing, or publishing it. Only playbooks published as **Public** appear in the Public tab. Ones you keep **Private** or **Unlisted** never show up there - see [visibility levels](/playbook/publish-and-share#step-1-publish-and-set-visibility). ## Public playbooks - browse and reuse The **Public** tab lists every publicly shared agent. Use it to start from someone else's working agent instead of a blank page. Playbook gallery showing Public and My Playbook tabs, a search box, and rows like Booking, Bank Agent Nisha, Airlines Ticket Booking Agent - each with a View in Playground action. Scan the list or use the **search** box to filter by name. No account needed just to browse. Hit **View in Playground**. The agent loads and you can **Press to talk** and hear it, exactly as its author built it. Use **Clone & edit** to copy it into your own **private** playbook. Now you can change the persona, checkpoints, and voice without touching the original. Cloning is the fastest way to learn playbook style - open a public agent that does something close to your use case, clone it, and reshape it in chat. If the Public tab says *"No public playbooks available yet,"* nothing has been published publicly in your environment yet - build one and [publish it Public](/playbook/publish-and-share) to seed the gallery. ## My Playbook - your own agents The **My Playbook** tab (login required) is your private library: everything you have created or cloned. Each row shows its state - a **Draft** tag marks a playbook you have saved but not published. My Playbooks tab in SuperDialog gallery showing a draft playbook card for Parkview Residences Site-Visit Booking with 0 journeys, 0 checkpoints, and an Open in playground link. **View in Playground** loads your playbook into the Editor + chat, right where you left it. Edit it in chat or by hand, test in Preview, and [publish or deploy](/playbook/publish-and-share) when it is ready. Empty tab? *"You don't have any private playbooks yet. Create one in the Playground."* Head to [Build an agent playbook](/playbook/build-an-agent) to make your first one. ## Switching playbooks inside the Playground You do not need to go back to the gallery to change agents. The top of the chat rail has a **playbook picker**: * Click it to open the dropdown, **search** by name, and **Select** any playbook you can access (yours, or a public one, tagged accordingly). * **New playbook** starts a fresh, blank agent. * A **history** affordance next to it lists your past **Conversations** (threads) for the current playbook, so you can reopen or start a **New chat**. Switching the picker changes which agent the Editor and Preview act on. A test turn in flight survives the switch, but save your work first so an unsaved buffer is not left behind. ## Where each one goes next Reshape it in chat - change persona, checkpoints, and sources. Press to talk and confirm it still behaves before you ship changes. Set it Public so it shows in the gallery, or Unlisted for a private link. Start with what a playbook is and why it beats a prompt or a graph. # Optimize Source: https://docs.unpod.ai/playbook/optimize Testing by hand covers the paths you think of. Optimize runs the agent against many simulated callers, scores goal completion, and hardens the weak checkpoints - the results stream into the chat as a live card. ## Why optimize When you press to talk you test the paths *you* think of. Real callers do things you did not script - they hesitate, change their mind, answer the wrong question. **Optimize** generates a spread of simulated callers, runs the conversation against each, scores how often the agent reached its goal, and rewrites the weak checkpoints to fix what failed. Optimize is powered by the builder engine's `generate_personas`, `simulate_and_score`, and `optimize_playbook` tools. They are **consent-gated** - the agent proposes a run and you accept it; it never silently rewrites your work. ## How to run it Use the **Optimize** action in the composer toolbar. The builder agent may also propose a run itself once your playbook looks functionally complete. An **Optimize run** card renders right in the chat thread with a live round indicator (`round 2/3 ●●○`) and a goal score that animates up as rounds complete - e.g. `62% → 81%`. On finish, the hardened YAML lands in the **Editor** tab ("Applied to editor"). Re-open Preview and re-test. Editor on the left with the booking playbook YAML; on the right the Agent panel shows a 'Run optimize_playbook?' card with Approve and Skip, a Test-run scorecard reading 100% Completion, 0% Data capture, 33% Smoothness, 0% Repairs, 100% Empathy, and a Voice LLM list (claude-haiku-4-5, gpt-4o-mini, gemma-4-31b-it, gpt-4.1-mini) with latency estimates. ## Reading the card The card has a `Details | Preview` toggle: The round-by-round trace and the **top failures in plain language** - "the agent skipped the callback-number checkpoint when the caller was in a hurry." The metric readout for the run (below), so you can see the score behind the animation. ## What gets measured The scorecard shows the engine's four deterministic metrics - computed from the transcript without any LLM call - plus one LLM-judged score: | Metric | Meaning | | ---------------- | ------------------------------------------------------------------------------------------------- | | **Completion** | How often the agent reached the playbook's outcome and closed cleanly. | | **Data capture** | Fraction of the required slots (order number, callback, ...) actually captured in the transcript. | | **Smoothness** | Penalises loops: drops as the agent burns extra turns per checkpoint. | | **Repairs** | Fraction of agent turns that are re-asks or corrections - lower is better. | | **Empathy** | LLM-judged response quality. | Latency is not scored by the optimizer - the per-model estimates in the Voice LLM picker come from real test calls. Coverage and knowledge-base-hit metrics on this card are later milestones. Guardrail scoring exists today in the [SuperDialog eval harness](/superdialog/evals), where it is a hard gate. The numbers you see here are real engine output, not estimates. ## Two ways it hardens the playbook `optimize_playbook` runs several rounds, each time regenerating personas, re-scoring, and rewriting the checkpoints that failed. The score climbs across rounds until it plateaus. The agent proposes prose edits in normal chat - "I'd tighten the refund checkpoint like this" - and you accept or reject each one. Give the optimizer a **goal string** to bias the run - e.g. "prioritize completing the sale in 8-10 turns." Presets like this let you optimize for the outcome you actually care about, not just raw completion. ## When to stop Stop when the goal-completion score plateaus and the top-failures list is empty or down to edge cases you are comfortable with. Do a final **Press to talk** pass in [Preview](/playbook/test-by-voice) to hear the hardened version, then ship it. ## Next step Set visibility, share the agent by link, clone it, and generate an API key to take it live on a number. # The Playground Source: https://docs.unpod.ai/playbook/playground The Playground is the no-code home for a playbook: describe an agent, get a working playbook, test it by voice, harden it, and ship it - all on one screen. What you hear here is what a caller hears. ## The no-code home for your playbook The **Playground** collapses a three-day authoring job into about thirty minutes. Describe an agent, get a working [playbook](/playbook/what-is-a-playbook), test it by voice, harden it, and take it live - **without leaving one screen.** It runs the same stack production does. The playbook is authored with **SuperDialog**; the browser voice call is served by the same `AgentRunner` that serves a real phone call. **What you hear in the Playground is what a caller hears.** How that seam is wired: [SuperDialog on Unpod voice](/superdialog/embedding-guides/unpod-voice). Open the Playground Launch it at superdialog.unpod.ai/playground, describe an agent, and start talking to it in your browser. A guest trial lets you build before you sign up. ## One screen, three surfaces You describe the agent in plain English ("confirm appointments, fall back to 5pm"). The builder agent authors and edits the playbook for you. Attach a PDF or paste a URL and it reads from those too. The live playbook YAML. Every change the chat makes lands here; you can also edit it by hand. Validation runs as you type. The voice test. Press to talk, speak like a caller, and watch the transcript, the current checkpoint, and the metrics update live. Preview tab showing the voice pipeline: User speaks into STT, a USER_TURN arrow into DialogMachine.turn() under 'SuperDialog - your flow', an AGENT_TURN arrow into TTS, and 'Caller hears' - with a 'Connected - speak to run a turn through the pipeline' status. ## The end-to-end journey Describe it in chat or start from a document; the agent writes the playbook. Press to talk in the browser and confirm it behaves like a real call. Run simulated personas, score goal completion, and harden the weak spots. Set visibility, share by link, clone, then deploy - as a voice agent on a number, or as an API endpoint. ## Reuse an existing agent You do not have to start from a blank page. The **Public** tab lets you open and clone any agent others published; the **My Playbook** tab holds your own drafts. ## Who this section is for | You are... | Start here | | ------------------------------------------------------------------ | ------------------------------------------------------------------------------- | | **A builder** (non-technical) who wants a working voice agent fast | [Build an agent playbook](/playbook/build-an-agent) - operate the Playground UI | | **A developer** who wants to call the agent from code | [Chat API](/playbook/api) - the OpenAI-compatible endpoint | | **Curious how the brain works** underneath | [SuperDialog](/superdialog/introduction) - the engine behind every playbook | The Playground is where you *build and prove* an agent. To put it in front of real callers, deploy the published agent as a **Voice Agent** on a [number](/speech-stack/numbers) (it wires a [Speech Pipe](/speech-stack/pipes) for you) - covered in [Publish & share](/playbook/publish-and-share). # Publish & share Source: https://docs.unpod.ai/playbook/publish-and-share A saved playbook is private to you. Publishing sets its visibility, hands out a share link, lets others clone it, and unlocks two ways to deploy - as a live voice agent on a number, or as a callable API endpoint. ## Save vs. publish **Saving** (from [Build an agent playbook](/playbook/build-an-agent)) keeps a playbook private to you as a draft. **Publishing** is the deliberate step that makes it available to others and unlocks deployment - either **as a voice agent on a phone number** or **as an API endpoint**. Until you publish, the top bar shows `Draft - not yet published`. This page starts after you have a working, saved playbook. If you have not saved yet - or want to move a playbook in or out as a file - see [Save, import & export](/playbook/build-an-agent#step-4-save-import-export) on the Build page. Publishing changes who can see and run your agent. Read the visibility levels below before you publish - "Public" lists the agent in a gallery anyone can find. ## Step 1 - Publish and set visibility Hit **Publish** in the top bar. You pick one of three visibility levels, shown by a badge on the playbook: | Visibility | Who can reach it | | ------------ | ----------------------------------------------------------------------- | | **Private** | Only you. | | **Unlisted** | Anyone with the link can open and test it - not discoverable otherwise. | | **Public** | Listed in the public gallery for anyone to find and test. | You can change visibility at any time; the badge updates and the setting is saved server-side. ## Step 2 - Share by link For an **Unlisted** or **Public** playbook, open **Share** to copy a link. Anyone who opens it lands in the Playground with your agent loaded and can talk to it - no account needed for the trial. If a link gets out, use **Regenerate** in the Share dialog to revoke the old link and issue a new one. Old links stop working immediately. Shared access is **view + run**, cross-tenant and project-scoped. Someone opening your link can test the agent and clone it; they cannot edit your original. ## Step 3 - Let others build on it (clone) Anyone viewing a shared or public playbook can **Clone & edit** to get their own private copy. It is the fastest way to hand a teammate a working starting point - they clone, tweak, and publish their own version without touching yours. ## Step 4 - Deploy Publishing opens the **deploy drawer**, which offers **two ways to ship** the same agent. Pick the one that matches how callers will reach it: Put it on a **real phone number**. Choose a number and the drawer wires up a speech pipe for you - inbound callers reach the agent immediately. Turn it into a **callable API** with an API key, so your own backend drives the agent. No phone number involved. Both run the **same published playbook and the same `AgentRunner`** you tested in Preview - nothing is re-implemented on the way to production. ### Deploy as Voice Agent Publish drawer on the Deploy as Voice Agent tab, showing a Phone Number picker with a number card and a rotate control, plus Cancel and Deploy as Voice Agent buttons. The drawer loads your available numbers in a picker - use the rotate control to cycle through them. Select one (or choose **No number** to deploy without assigning one yet). On deploy the platform assigns the number and provisions a **speech pipe** behind it - you get a "Deployed!" confirmation with the wired pipe. Dial the number, or dispatch an outbound call to it. The agent answers with the playbook you tested. Need a number first, or want to manage pipes by hand? See [Numbers](/speech-stack/numbers) and [Speech Pipes](/speech-stack/pipes) - the Voice Agent deploy is the one-click version of that wiring. ### Deploy as Endpoint Turns the agent into an **OpenAI-compatible** `chat/completions` API. The drawer shows your **endpoint**, the **model** (your playbook id), and a ready-to-run snippet in cURL, Python, JavaScript, and Go. Publish drawer on the Deploy as Endpoint tab, showing the inference.unpod.ai endpoint, a public: model id, Manage API Keys, and cURL / Python / JavaScript / Go code tabs. Hit **Deploy as Endpoint**, then **Manage API Keys** to mint one. Copy it now - treat it like a password. Rotate it any time if it leaks. Pick your language tab. Each snippet is pre-filled with your endpoint, your `model` id, and the `Authorization: Bearer $UNPOD_API_KEY` header. Point any OpenAI SDK at the base URL. The agent runs the exact playbook you tested in Preview. ```bash Example theme={null} curl https://inference.unpod.ai/v1/chat/completions \ -H "Authorization: Bearer $UNPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"public:PB_7ZRMzCA1ojQ9LlcK","messages":[{"role":"user","content":"hi"}]}' ``` API keys carry your account's access. Store them in a secret manager or environment variable (`UNPOD_API_KEY`), never in client-side code or a committed file. Revoke and regenerate immediately if one is exposed. Headers, request body, stateful sessions via `user`, and cURL / Python / JavaScript / Go examples. ## Bring your own key (BYOK) If you want calls billed to **your** provider account instead of the Playground wallet, open the **BYOK** panel and paste keys for your LLM and speech providers (OpenAI, Deepgram, Cartesia, ElevenLabs, ...). When set, the Playground routes through your keys; **Clear** reverts to the shared wallet. Guest and trial usage draws on a metered wallet - you will see a balance chip and an "out of minutes" prompt when it runs low. BYOK sidesteps that by using your own provider quota. # Test by voice Source: https://docs.unpod.ai/playbook/test-by-voice The Preview tab is a real voice call in your browser. Press to talk, speak like a caller, and watch the transcript, the live checkpoint, and the metrics - served by the same runtime a phone call uses. ## Preview is the test There is no separate "test mode" and no phone number needed. The **Preview** tab runs a live voice session against your current playbook using the same `AgentRunner` that serves a production call. What you hear here is what a caller hears. Your microphone streams up at 16 kHz; the agent's synthesized voice streams back at 24 kHz. The audio rides a WebSocket bridge; the transcript and checkpoint arrive on a **separate** side channel, so the transcript stays accurate even when audio is under load. ## Step 1 - Set the voice and model The Preview header has the run controls: Pick the voice the agent speaks in - the same voice a real caller would hear. Each profile shows its **language**, its **STT + TTS providers**, an estimated **cost per turn**, and a **WER** (word-error-rate) quality hint. Choose the LLM the **agent** runs on for this test (separate from the builder model in the chat). The picker shows speed/cost hints like `GPT-4.1 mini · fast`. **BYOK** lets you run on your own provider key. Preview voice test: a voice profile dropdown listing Anika, Neha, Shagun, Riya, Zara, Pooja - each with a language tag, STT and TTS provider, cost per turn, and WER - next to a large Press to talk mic orb with 'Try saying:' chips. If **Press to talk** is disabled, the header tells you why - usually a missing voice profile or an unsaved playbook. Pick a voice and save, and the control unlocks. ## Step 2 - Press to talk The center of the Preview is a single **Press to talk** control. You see the orb plus a row of **"Try saying:"** opener chips - example first lines pulled from your playbook. They are prompts for *you*; they are not injected into the agent. Press it, allow the mic, and talk as a caller would. The session connects and the agent answers in the voice you chose. The transcript fills in turn by turn with timestamps, a slim pipeline readout shows the current stage, and level meters confirm audio is flowing. Preview tab showing the voice pipeline in action ## Step 3 - Read the live signal While you talk, three things tell you whether the playbook works: | Surface | What it tells you | | ---------------------- | ---------------------------------------------------------------------------------------- | | **Transcript** | The exact turns, so you can see where the agent misread you. | | **Current checkpoint** | Which checkpoint the conversation is in, and whether it advanced when it should have. | | **Metrics row** | A compact `ttfa · turns · cost` readout - time to first audio, turn count, and run cost. | ### What to actually check * The **persona and voice** sound right to a caller. * Checkpoints **advance on the conditions you wrote** (`done_when` / `advance_when`) - not too early, not too late. * **Slot collection** works: the agent captures what it needs (order number, callback) before moving on. * **Interrupts** fire from any step - saying "goodbye" or "I'm busy" ends the call gracefully. ## Step 4 - Dig in when something is off Open the **advanced drawer** below the call for the detailed console: the EVENTS log, traces, and per-turn LLM calls. Use it to see *why* a checkpoint did not advance - the Director's judgment and any tool calls are all there. The console shows the engine's own events and never leaks raw provider errors. If a turn fails, you get a clean surface here, not a stack trace. ## The tight loop Testing is meant to feed straight back into authoring - both panes stay mounted, so a turn in flight survives a tab switch: e.g. the agent offers 5pm before the caller declines 4pm. "Only offer 5pm after the caller says 4pm doesn't work." The YAML updates. Re-test the same path. Repeat until it behaves. ## Next step When it behaves on the paths you tried by hand, let the optimizer stress it against many simulated callers. Run generated personas, score goal completion, and harden the weak spots automatically. # What is a playbook Source: https://docs.unpod.ai/playbook/what-is-a-playbook A playbook is a short, plain-language document that tells the agent who it is, what it must accomplish, and when a step is done. Checkpoints gate outcomes, not utterances - and a fast Talker plus an async Director run the whole thing. ## In one sentence A **playbook** describes an outcome-driven conversation in plain language. Instead of drawing a flow chart with every branch, or stuffing everything into one prompt, you write **checkpoints** - the things that must be true before the conversation moves on. ## Why not a big prompt or a graph A big prompt fails by **confusion** - the longer it grows, the more the model skips rules. A hand-drawn graph fails by **latency and rigidity** - extra model calls per node, and real callers walk off your edges. | | Big prompt | Rigid graph | **Playbook** | | --------------------- | ------------------------ | ----------------------- | ------------------------------------- | | **Steering** | One long prompt | Every edge hand-drawn | Checkpoints - declare the outcome | | **Off-script caller** | Drifts | Hits an unhandled state | Engine finds the path | | **Latency** | One call per turn | Extra calls per node | Talker streams, Director judges async | | **Changing it** | Rewrite the wall of text | Rewire the graph | Add or edit a checkpoint | The full model - and when a graph still earns its place - is in [Thinking in playbooks](/superdialog/thinking-in-playbooks). ## Checkpoints gate outcomes, not utterances The engine decides what to *say* each turn. The checkpoints decide when the conversation is allowed to *move on*. You describe the destination; you do not script every sentence. ```yaml Example checkpoint theme={null} - id: confirm_appointment purpose: Confirm the caller's Friday 4pm appointment. say: Offer Friday 4pm first, fall back to 5pm. done_when: The caller has said yes to a time, or asked to reschedule. ``` That is the whole mental model: **checkpoints gate outcomes, not utterances.** ## The parts of a playbook | Part | What it is | | -------------------------------- | ----------------------------------------------------------------------------------------------- | | **Persona** | Who the agent is and how it sounds - the voice and tone a caller hears. | | **Opening** | The first line the agent says when the conversation starts. | | **Steps (checkpoints)** | The ordered outcomes that must be met. Each has a `purpose` and a `done_when`. | | **`done_when` / `advance_when`** | The plain-language condition that lets the conversation leave a checkpoint. | | **Slots** | The pieces of data to capture along the way (order number, callback, a chosen time). | | **Interrupts** | Conditions that can fire from any step - "goodbye", "I'm busy" - to end or redirect gracefully. | Field-by-field reference for both authoring formats is in [Playbooks](/superdialog/playbooks). ```yaml A small, complete playbook theme={null} goal: Confirm the caller's appointment for Dr. Lee's clinic. persona: name: Mira identity: A warm, efficient scheduling assistant for Dr. Lee's clinic. voice_style: Calm, brisk, never pushy. opening: "Hi, this is Dr. Lee's office calling to confirm your appointment." playbook: - id: confirm_time purpose: Confirm the Friday 4pm slot, or offer 5pm. say: Offer Friday 4pm first, fall back to 5pm. done_when: The caller accepts a time or asks to reschedule. - id: wrap_up purpose: Read back the confirmed time and say goodbye. done_when: The caller has heard the confirmed time. terminal: true outcome: confirmed ``` ## How it runs: Talker + Director A playbook is fast because two roles run at once: Streams every spoken reply the moment it can, so the caller hears a natural, immediate response - no waiting on bookkeeping. Runs **asynchronously**: reads the transcript, extracts slot data, judges whether a checkpoint's `done_when` is met, and runs any tools - over an event-sourced log. Turn ordering, the event-sourced log, and why the Director never blocks the Talker are covered in [Architecture](/superdialog/architecture). Go deeper on `done_when` / `advance_when`, slots, and interrupts in the mental-model guide. ## Where playbooks come from You rarely hand-write the YAML from scratch. You author a playbook by **talking to a builder agent in the Playground** - describe the job, and it writes the checkpoints for you. The browser app where you author, test, optimize, and ship a playbook. The hands-on walkthrough: describe an agent and watch its playbook appear. # Adapters Reference Source: https://docs.unpod.ai/speech-stack/adapters The DialogAdapter protocol, every bundled adapter signature, auto-wrapping rules, and error surfaces. Reference for the brain slot. For worked examples and guidance, see [Bring Your Agent](/speech-stack/bring-your-agent); this page is the contract. ## The DialogAdapter protocol Defined in `unpod.adapters.base`. Runtime-checkable - any object with these three methods passes `isinstance(obj, DialogAdapter)`; no base class required. ```python theme={null} class DialogAdapter(Protocol): async def turn(self, text: str, context: dict | None = None) -> str: """Return a complete response string. NOT called during live calls.""" async def stream( self, text: str, context: dict | None = None, language: str | None = None ) -> AsyncIterator[str]: """Yield response tokens. HOT PATH - session.run() calls this on every user turn and streams tokens directly to the voice bridge. `language` is the per-turn language code (from `UserTextEvent.extra`), forwarded by `session.run()`. Custom adapters MUST accept the kwarg even if they ignore it.""" def assist(self, text: str) -> None: """Inject a system instruction before the next turn.""" ``` `stream()` now receives a `language` keyword argument on every turn. Any custom adapter whose `stream()` does not accept `language=...` raises `TypeError` on the first user turn. Add `language: str | None = None` to the signature. `session.run()` calls only `stream()`. A single-chunk `stream()` fallback (await the full reply, yield once) produces choppy, high-latency audio with no error anywhere. See [Streaming is the hot path](/speech-stack/bring-your-agent#streaming-is-the-hot-path). ## Auto-wrapping The `ctx.session.dialog_machine` setter accepts, in order: 1. A superdialog `DialogMachine` or `LLMAgent` - wrapped in a `SuperDialogAdapter` automatically. 2. Any object satisfying the `DialogAdapter` protocol - used as-is. 3. Anything else - raises `TypeError`. ## Bundled adapters All importable from `unpod.adapters`: `AnthropicAdapter`, `DialogAdapter`, `HTTPAdapter`, `LangChainAdapter`, `MCPAdapter`, `OpenAIAdapter`, `SuperDialogAdapter`. ### Constructor signatures | Adapter | Constructor | Wraps | | -------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------- | | `OpenAIAdapter` | `(client, model="gpt-4o-mini", system_prompt=None)` | `openai.AsyncOpenAI` client | | `AnthropicAdapter` | `(client, model="claude-haiku-4-5-20251001", system_prompt=None, max_tokens=1024)` | `anthropic.AsyncAnthropic` client | | `LangChainAdapter` | `(chain, input_key="messages")` | LangChain Runnable with `ainvoke`/`astream` | | `HTTPAdapter` | `(url, headers=None, timeout_s=10.0)` | A remote HTTP endpoint, any language | | `MCPAdapter` | `(server_url, tools=None, llm="anthropic/claude-haiku-4-5", headers=None)` | An MCP server (preview - see below) | | `SuperDialogAdapter` | `(dm)` | superdialog `DialogMachine` or `LLMAgent` | ### Streaming behavior | Adapter | `stream()` behavior | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `OpenAIAdapter` | Real token streaming via `chat.completions.create(stream=True)` | | `AnthropicAdapter` | Real token streaming via `messages.stream()` | | `LangChainAdapter` | Real streaming via the chain's `.astream()` | | `SuperDialogAdapter` | Delegates to `dm.turn(text, stream=True[, language=…])` - `language` is passed through only when the wrapped machine's `turn()` accepts it | | `HTTPAdapter` | **Single chunk** - one POST round-trip, full reply yielded once | | `MCPAdapter` | Falls back to `turn()`, which is not implemented yet | ### `assist()` semantics | Adapter | What `assist(text)` does | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------- | | `OpenAIAdapter` / `LangChainAdapter` | Injects the instruction into the conversation history | | `AnthropicAdapter` | Injects as a user/assistant message pair - the Anthropic API does not accept mid-conversation system messages | | `HTTPAdapter` | Queues the instruction; sent as `system_instructions` in the next request body | | `MCPAdapter` | Queues the instruction (pending full implementation) | | `SuperDialogAdapter` | Delegates to the DialogMachine's `assist()` | ### SuperDialogAdapter extras Beyond the protocol, `SuperDialogAdapter` exposes the wrapped agent's controls: ```python theme={null} adapter.set_llm("anthropic/claude-haiku-4-5") # swap the runtime model adapter.switch_flow("billing", preserve_memory=False) # graph engine only (FlowSet) adapter.is_complete # reached a terminal state? adapter.state # current state (dict) ``` `is_complete` and `state` work on both engines; on the Playbook engine `state` returns `{"checkpoint": ..., "slots": ..., "ended": ...}`. `switch_flow` applies only to the legacy graph engine (a `DialogMachine` built from a `FlowSet`). See [SuperDialog](/superdialog/introduction) for the agent itself. ## Error surfaces | Failure | What you see | | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | Assigning a non-adapter to `dialog_machine` | `TypeError` from the setter, immediately | | `HTTPAdapter` endpoint returns non-2xx | `httpx.HTTPStatusError` (the adapter calls `raise_for_status()`) | | `HTTPAdapter` endpoint slower than `timeout_s` | `httpx.TimeoutException` | | `MCPAdapter` without the `mcp` package | `ImportError` - install with `unpod[mcp]` | | `MCPAdapter` with the package | `NotImplementedError` - full MCP orchestration is not implemented yet | | `LangChainAdapter` with a chain expecting a different input shape | No error - the chain receives `{input_key: history}` and may silently misbehave; set `input_key` to match your chain | | Lazy `stream()` in a custom adapter | No error - choppy, delayed audio on calls | # AgentRunner & Sessions Source: https://docs.unpod.ai/speech-stack/agent-runner The Connectivity API runtime: run an AgentRunner, control live calls with Session, and read per-call metrics. The Connectivity half of the SDK. An [AgentRunner](/get-started/core-concepts#agentrunner) is the long-lived process that receives calls; a [Session](/get-started/core-concepts#session) is your control surface for one live call. This page is the reference for both. ## Install ```bash theme={null} uv add unpod uv add "unpod[dialog]" # optional: superdialog for structured flows uv add "unpod[langchain]" # optional: LangChain adapter ``` Source: [unpod-ai/unpod-python-sdk](https://github.com/unpod-ai/unpod-python-sdk). ```bash theme={null} export UNPOD_API_KEY="sk_..." export UNPOD_BASE_URL="api.unpod.ai" # one URL; the runner derives wss:// ``` The [Quickstart](/get-started/quickstart#step-2-set-two-keys) documents every environment variable. ## The runner `AgentRunner` holds a WebSocket connection to the Unpod orchestrator. When a call is dispatched to your agent, the runner invokes your `entrypoint` with a `CallContext`. Animated AgentRunner dispatch diagram showing the Unpod orchestrator connected to AgentRunner over WSS, heartbeats and capacity reporting, dispatch into entrypoint CallContext, and the Session run loop. ```python theme={null} from unpod import AgentRunner, CallContext async def handle_call(ctx: CallContext) -> None: await ctx.session.say("Hello, thanks for calling!") await ctx.session.run() # blocks until the call ends runner = AgentRunner( entrypoint=handle_call, agent_id="my-agent", # must match agent_id in your Speech Pipe ) runner.start() # blocking ``` `agent_id` is the runner agent ID - a short string you choose, matching the `agent_id` in your Speech Pipe config. Not the pipe's UUID. See [IDs You'll Meet](/get-started/core-concepts#ids-youll-meet). ### Constructor ```python Signature theme={null} AgentRunner( entrypoint: Callable[[CallContext], Awaitable[None]], agent_id: str, api_key: str | None = None, # falls back to UNPOD_API_KEY max_sessions: int = 50, # max concurrent sessions max_concurrent_calls: int | None = None, # alias for max_sessions permits_per_minute: int = 120, # rate of new call acceptance drain_timeout_s: int = 60, # graceful shutdown window dev_mode: bool = False, # local orchestrator, dev pool base_url: str | None = None, # override orchestrator URL serving_url: str | None = None, # serve transport only; falls back to UNPOD_RUNNER_URL agent_secret: str | None = None, # serve transport only; falls back to UNPOD_AGENT_SECRET transport: str = "dial_out", # "dial_out" (v2 default) | "serve" (legacy) ) ``` | Parameter | Description | | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `max_sessions` | Max simultaneous sessions this runner accepts; the orchestrator will not dispatch beyond it. | | `permits_per_minute` | Rate of new call acceptance. Lower it to protect downstream systems. | | `drain_timeout_s` | On shutdown, wait this long for active calls to finish before force-exiting. | | `dev_mode` | Register in a dev pool against a local orchestrator. | | `transport` | `"dial_out"` (v2, default): the runner never listens - it dials **out** per call to the `bridge_url` delivered in the `job.assign` frame. `"serve"` (legacy, deprecated): the runner hosts a bridge server that media agents dial into. | | `serving_url` | Public `wss://` URL of the runner's bridge server. Only used with `transport="serve"`; **ignored** (warns) under the default `dial_out` transport, which never listens. | | `agent_secret` | Only used with `transport="serve"`: when set, inbound bridge connections are HMAC-verified; without it the runner accepts unsigned connections (dev default). **Ignored** (warns) under the default `dial_out` transport. | ### Runner lifecycle hooks React to runner-level events (distinct from per-session hooks): ```python theme={null} @runner.on("call_start") async def on_call_start(ctx: CallContext) -> None: print(f"New call: {ctx.call_id}") @runner.on("call_end") async def on_call_end(ctx: CallContext, final_state: str) -> None: print(f"Call {ctx.call_id} ended: {final_state}") ``` This is the **runner-level** `call_end` (signature `(ctx, final_state)`), fired once per call on the runner. The **session-level** `call_end` (`@ctx.session.on("call_end")`, signature `(final_state)`) fires inside a single call and is where you read the telephony `end_reason` via `ctx.session.data.get("end_reason")`. See [Call Lifecycle](/speech-stack/call-lifecycle#reacting-to-outcomes-in-your-agent). ## CallContext Every call to your `entrypoint` receives a [CallContext](/get-started/core-concepts#callcontext): ```python theme={null} async def handle_call(ctx: CallContext) -> None: ctx.call_id # str: unique call ID ctx.session_id # str: unique session ID ctx.agent_id # str: the CALL's agent (from the dispatch / call.started) ctx.runner_id # str: this runner's OWN configured agent_id ctx.direction # str: "inbound" or "outbound" ctx.user_number # str: caller's E.164 number ctx.instructions # str | None: per-call override instructions ctx.data # dict: metadata from dispatch (e.g. CRM data) ctx.room # dict: LiveKit room metadata (informational; brain is text-only) ctx.session # Session: call control object ``` `ctx.agent_id` is the agent the **call** was dispatched for (carried on `call.started`); `ctx.runner_id` is the agent\_id this runner process was constructed with. They match for a single-agent runner and differ for a multi-tenant one - route on `ctx.agent_id`, not `ctx.runner_id`. ## The Session `ctx.session` is your interface to the live call: speak, interrupt, transfer, record, end - all from inside your entrypoint. ### Speaking ```python theme={null} await ctx.session.say("Thank you for your patience.") # speak via TTS, returns immediately await ctx.session.set_filler("One moment please...") # played during processing silences ``` ### Interrupting ```python theme={null} @ctx.session.on("user_turn") async def on_user_turn(text: str) -> None: if "stop" in text.lower(): await ctx.session.interrupt() # stop the current utterance ``` ### Transferring ```python theme={null} await ctx.session.transfer_to_human(queue="tier-2-support") # cold transfer to a human queue await ctx.session.transfer_to_agent(agent_id="billing-agent") # cold transfer to another agent ``` A cold transfer drops your session the moment it is initiated. For a warm handoff, use the out-of-band `client.sessions.transfer(..., mode="warm")` - see below. ### Ending ```python theme={null} await ctx.session.end(reason="completed") # reason defaults to "completed" ``` Common reasons: `"completed"`, `"no_response"`, `"error"`, `"transferred"`, `"max_duration"`. ### Recording control ```python theme={null} await ctx.session.recording.pause(reason="PII") # e.g. before card numbers await ctx.session.recording.resume() ``` Pause/resume requires recording to be enabled on the Speech Pipe (`recording=True`); otherwise these calls are ignored. ### Per-call data `session.data` is a plain dict scoped to the current call: ```python theme={null} ctx.session.data["customer"] = await crm.lookup(ctx.user_number) ``` ### The main loop - `run()` `session.run()` keeps the call alive. It reads bridge events, fires your hooks, routes each transcribed user turn to your dialog adapter's `stream()`, and pipes the reply tokens to TTS. ```python theme={null} async def handle_call(ctx: CallContext) -> None: ctx.session.dialog_machine = my_brain # see Bring Your Agent await ctx.session.say("Hi, I'm Alex. How can I help?") await ctx.session.run() # blocks until call ends # anything here runs as post-call cleanup ``` ### Live metrics ```python theme={null} m = ctx.session.metrics.live() # CallMetrics snapshot, during or after run() m.turns # int: dialog turns so far m.duration_s # float: call duration m.stt_p95_ms # int: P95 STT latency m.llm_p95_ms # int: P95 LLM latency m.tts_p95_ms # int: P95 TTS latency m.cost.voice # float - m.cost.llm, m.cost.total m.tokens.input # int - m.tokens.output m.active_llm # str: model used on the last turn ``` Latency/cost/token fields populate only if you feed the tracker via `metrics.record_turn(...)` (e.g. from the `turn_complete` hook). Out of the box only `duration_s` is meaningful - for real per-turn numbers use the `llm_call` / `turn_complete` hooks. See [Metrics, Cost & Observability](/speech-stack/observability). ### Session API reference | Method | Signature | Description | | ------------------- | ------------------------------------------ | --------------------------------------------- | | `say` | `async (text: str) → None` | Speak text via TTS | | `interrupt` | `async () → None` | Stop current utterance | | `set_filler` | `async (text: str) → None` | Set filler phrase | | `transfer_to_human` | `async (queue: str) → None` | Cold transfer to human queue | | `transfer_to_agent` | `async (agent_id: str) → None` | Cold transfer to another agent | | `end` | `async (reason: str = "completed") → None` | End the call | | `run` | `async () → None` | Main event loop | | `on` | `(event: str) → decorator` | Register hook | | `recording.pause` | `async (reason: str = "") → None` | Pause recording | | `recording.resume` | `async () → None` | Resume recording | | `metrics` | `property → MetricsTracker` | Per-call metrics (`.live()`) | | `dialog_machine` | `property (get/set)` | Dialog adapter - auto-wraps superdialog types | | `data` | `dict[str, Any]` | Per-call scratch space | ## Out-of-band session control Act on a live session from outside the call - your backend, an ops tool - via the Management SDK, targeting it by session ID: ```python theme={null} from unpod import AsyncClient async with AsyncClient() as client: # reads UNPOD_API_KEY; see setup-checklist for the full credential table await client.sessions.end(session_id) await client.sessions.transfer( # warm handoff supported here session_id, to_type="sip", to_config={"number": "+15551230000"}, mode="warm", warm_handoff_ms=4000, ) await client.sessions.merge( # e.g. conference a supervisor in primary_session_id, secondary_session_ids=[other_session_id], ) ``` ## Running in production ### Monitoring ```python theme={null} s = runner.stats() # RunnerStats snapshot s.in_flight # current active calls s.queued # dispatches waiting for capacity s.capacity # your max_sessions setting s.completed_last_hour # completed calls s.failed_last_hour # failed calls s.mean_call_duration_s # average call length ``` ### Graceful shutdown Send `SIGTERM` (standard for containers and systemd). The runner stops accepting dispatches, waits up to `drain_timeout_s` for active calls, then exits. Or call `await runner.shutdown()` yourself. ### Multiple runners Run multiple `AgentRunner` processes with the same `agent_id` across machines. The orchestrator load-balances on reported capacity - no shared state needed. ## Next steps Plug your existing brain into session.dialog\_machine. React to every turn, interruption, silence, and lifecycle event. # Bring Your Agent Source: https://docs.unpod.ai/speech-stack/bring-your-agent Connect your existing LangChain, OpenAI, Anthropic, HTTP, or custom Python agent to Unpod voice infrastructure. Already have a working agent? This is your page. You wrap your brain in a `DialogAdapter`, assign it to `ctx.session.dialog_machine`, and Unpod handles the rest of the call - telephony, speech-to-text, text-to-speech, audio transport. Your [Agent](/get-started/core-concepts#agent-brain) stays text-in/text-out. If you don't have an agent yet, start with the [Quickstart](/get-started/quickstart) instead - it builds the minimal brain first. ## Install ```bash theme={null} uv add unpod ``` ## Write your entrypoint The entrypoint is an async function called once per call. Set your agent on `ctx.session.dialog_machine` and call `await ctx.session.run()`. ```python theme={null} from unpod import AgentRunner, CallContext from unpod.adapters.langchain import LangChainAdapter # Your existing LangChain chain from your_app import chain async def entrypoint(ctx: CallContext) -> None: ctx.session.dialog_machine = LangChainAdapter(chain) await ctx.session.run() ``` `AgentRunner` requires `UNPOD_API_KEY` in your environment, and the `agent_id` you pass must match your Speech Pipe's `agent_id` - see [IDs You'll Meet](/get-started/core-concepts#ids-youll-meet). The [Provisioning checklist](/speech-stack/setup-checklist#environment-variables) lists every variable. ```python theme={null} AgentRunner( entrypoint=entrypoint, agent_id="my-agent", # must match agent_id in your Speech Pipe config ).start() ``` Incoming calls to your Unpod number are now routed to your agent. ## Streaming is the hot path This is the single most important thing on this page. During a live call, `session.run()` calls your adapter's **`stream()`** on every user turn and pipes tokens straight to the voice bridge for synthesis. **`turn()` is never called by the framework during live calls.** If your adapter implements `turn()` properly but fakes `stream()` with a single-chunk fallback (await the full response, yield it once), the caller hears **silence until the entire response is generated, then a long monologue** - choppy, high-latency audio with no error message anywhere. Implement real token streaming in `stream()`. The bundled adapters differ here - check the table below before choosing: `OpenAIAdapter`, `AnthropicAdapter`, and `LangChainAdapter` stream real tokens from their providers; `HTTPAdapter` cannot stream (one HTTP round-trip, one chunk), so expect a latency penalty proportional to your response length. ## The DialogAdapter protocol Any object with these three methods is a valid brain - no base class required (the protocol is runtime-checkable): ```python Signature theme={null} async def turn(self, text: str, context: dict | None = None) -> str # Return a complete response. Not called during live calls. async def stream(self, text: str, context: dict | None = None, language: str | None = None) -> AsyncIterator[str] # Yield response tokens. THE hot path - session.run() calls this. # `language` is the per-turn language code (from UserTextEvent.extra). # Custom adapters MUST accept the kwarg even if they ignore it, else # session.run() raises TypeError on the first turn. def assist(self, text: str) -> None # Inject a system instruction before the next turn. ``` **Auto-wrapping:** assigning a superdialog `DialogMachine` or `LLMAgent` directly to `ctx.session.dialog_machine` wraps it in a `SuperDialogAdapter` for you. Anything else must satisfy the protocol above, or the setter raises `TypeError`. ## Supported adapters | Adapter | Import | Real streaming | Use when | | -------------------- | ---------------------------- | ----------------- | ------------------------------------------------------------------------------- | | `OpenAIAdapter` | `unpod.adapters.openai` | Yes | OpenAI `AsyncOpenAI` client - `gpt-4o`, `gpt-4o-mini`, etc. | | `AnthropicAdapter` | `unpod.adapters.anthropic` | Yes | Anthropic `AsyncAnthropic` client - Claude models | | `LangChainAdapter` | `unpod.adapters.langchain` | Yes | LangChain chain with `.ainvoke()` / `.astream()` | | `SuperDialogAdapter` | `unpod.adapters.superdialog` | Yes | superdialog `DialogMachine` / `LLMAgent` - or assign directly, auto-wrapped | | `HTTPAdapter` | `unpod.adapters.http` | No - single chunk | Remote agent API (any language); accepts the latency trade-off | | `MCPAdapter` | `unpod.adapters.mcp` | Preview | Interface defined; full MCP orchestration is not implemented yet (`unpod[mcp]`) | | Custom | Implement the protocol | Up to you | Any Python object with `turn`, `stream`, `assist` | ## OpenAI ```python theme={null} from openai import AsyncOpenAI from unpod import AgentRunner, CallContext from unpod.adapters.openai import OpenAIAdapter client = AsyncOpenAI() async def entrypoint(ctx: CallContext) -> None: ctx.session.dialog_machine = OpenAIAdapter( client=client, model="gpt-4o-mini", system_prompt="You are a helpful support agent. Be concise.", ) await ctx.session.run() AgentRunner(entrypoint=entrypoint, agent_id="my-agent").start() ``` ## Anthropic (Claude) ```python theme={null} import anthropic from unpod import AgentRunner, CallContext from unpod.adapters.anthropic import AnthropicAdapter client = anthropic.AsyncAnthropic() async def entrypoint(ctx: CallContext) -> None: ctx.session.dialog_machine = AnthropicAdapter( client=client, model="claude-haiku-4-5-20251001", system_prompt="You are a helpful support agent. Be concise.", ) await ctx.session.run() AgentRunner(entrypoint=entrypoint, agent_id="my-agent").start() ``` Model name formats differ by layer: direct adapters take the provider's raw model name (`claude-haiku-4-5-20251001`, `gpt-4o-mini`); superdialog brains take a `provider/model` URI (`anthropic/claude-haiku-4-5`). Both are correct in their context. ## LangChain `LangChainAdapter` expects your chain to accept `{"messages": [...]}` as input by default. This works with `ChatPromptTemplate | ChatModel` chains. The adapter keeps conversation history across turns and streams via `.astream()`. If your chain uses a different input key, pass `input_key`: ```python theme={null} # Chain expects {"input": "..."} LangChainAdapter(chain, input_key="input") ``` ## HTTP endpoint `HTTPAdapter` lets you keep your agent in any language behind an HTTP API. Each user turn becomes one `POST`: ```python theme={null} from unpod.adapters.http import HTTPAdapter adapter = HTTPAdapter( url="https://agent.example.com/respond", headers={"Authorization": "Bearer ..."}, # optional timeout_s=10.0, ) ``` Request body your endpoint receives, and the response it must return: ```json theme={null} // request - session_id and system_instructions are included when present {"text": "what the caller said", "context": {}, "session_id": "...", "system_instructions": ["..."]} // response {"text": "your agent's reply"} ``` `HTTPAdapter` does not stream - the whole reply arrives as one chunk, so the caller waits for your full HTTP round-trip before hearing anything. Fine for short replies; for long-form answers prefer an in-process adapter. ## Using session controls Inside your entrypoint you can react to call events and control the call: ```python theme={null} async def entrypoint(ctx: CallContext) -> None: @ctx.session.on("user_turn") async def _(text: str) -> None: print(f"User said: {text}") @ctx.session.on("call_end") async def _(reason: str) -> None: print(f"Call ended: {reason}") ctx.session.dialog_machine = LangChainAdapter(chain) await ctx.session.run() ``` Common hooks: `call_start`, `user_turn`, `agent_turn`, `user_partial`, `interruption`, `call_end`, `error` (the registry also accepts `tool_call`, `tool_result`, `silence`, `state`, `metric`, `llm_call`, and `turn_complete`). See [Session](/get-started/core-concepts#session) for the full control surface. ## Writing a custom adapter Implement the three protocol methods - no base class required: ```python theme={null} class MyAdapter: async def turn(self, text: str, context: dict | None = None) -> str: """Return complete response. Called for non-streaming use.""" return my_agent.respond(text) async def stream( self, text: str, context: dict | None = None, language: str | None = None ): """Yield response tokens. THIS is the hot path used by session.run(). Accept `language` even if unused - session.run() always passes it.""" async for token in my_agent.stream(text): yield token def assist(self, text: str) -> None: """Inject a system instruction before the next turn.""" my_agent.set_instruction(text) ``` ## Next steps One-time resource provisioning: agent, number, voice profile say(), transfer(), recording controls during live calls Conversation runtime: playbooks (default) and flow graphs Plug a SuperDialog agent directly into your AgentRunner session # Call Lifecycle & States Source: https://docs.unpod.ai/speech-stack/call-lifecycle How a call flows end to end, every state it moves through, voicemail detection, and the end reasons your agent observes. Every call - whether you start it or a caller rings in - ends up the same way: a Unpod media worker and the caller are the two participants in a LiveKit room, and the worker bridges the audio while streaming transcripts to your `AgentRunner` over a separate text-only bridge. Your dialog brain never joins the room or touches audio. This page traces both directions, the states a call moves through, and how it can end. Animated call lifecycle diagram showing a caller entering Unpod, speech converted to text, dispatch to AgentRunner and Session, dialog turns, reply text, and final transcript, metrics, and webhook storage. ## The state machine ```mermaid theme={null} stateDiagram-v2 [*] --> pending: calls.create() (outbound) [*] --> ringing: inbound call arrives pending --> ringing: dialing starts ringing --> active: answered ringing --> failed: no answer / busy / blocked / cancelled / voicemail pre-connect active --> completed: hangup or end() active --> failed: error completed --> [*] failed --> [*] ``` | Visible `status` | Internal state | What is happening | | ---------------- | ----------------------------- | ------------------------------------------------------------ | | `pending` | queued | Enqueued; waiting for a concurrency slot (outbound only). | | `ringing` | `dialing` / `ringing` | The SIP leg is originating, or the far end is ringing. | | `active` | `active` | Both legs connected; your agent is talking to the caller. | | `completed` | `ended` | The conversation finished after being active. | | `failed` | `not_connected` / `cancelled` | Never reached a live conversation, or cancelled pre-connect. | Inbound calls skip `pending` and dialing - the caller is already on the line, so the call starts at `ringing`/`active`. Only outbound calls pass through the full sequence. You never drive these states yourself; you observe them via the call `status`, the `call_end` hook, and the final `end_reason`. ## Outbound: what happens after `calls.create()` `calls.create()` is **asynchronous**. It does not wait for the phone to ring - it puts your call on a queue and returns immediately with `status: "pending"`. The actual dialing happens in the background, gated by your plan's per-account concurrency limit. `calls.create(pipe_id, to_number, ...)` records the call and returns `201` with `status: "pending"`. Your request is never blocked on the network. A background worker picks up the call. If your account is at its concurrent-call cap, the call is automatically rescheduled and retried shortly - no error, no dropped call. Unpod creates a room and originates the SIP call to `to_number`. The call row moves to `ringing`, and you get a `session_id`. A Unpod media worker joins the room and bridges the caller's audio; transcripts stream to your `AgentRunner` over its text bridge and reply text comes back for TTS. The call row moves to `active`, then `completed` on hangup. ```mermaid theme={null} sequenceDiagram participant You as Your backend participant API as Unpod API participant Q as Queue (concurrency-gated) participant LK as LiveKit + SIP participant Agent as Your AgentRunner You->>API: calls.create(pipe_id, to_number) API-->>You: 201 { status: pending } API->>Q: enqueue Q->>Q: per-account concurrency check Q->>LK: create room + dial to_number LK-->>Agent: media worker bridges call; runner connects (text) LK-->>Agent: caller transcript (text) Agent-->>LK: reply text (synthesised to speech on Unpod's side) Note over API: status: pending → ringing → active → completed ``` Because `create()` returns `pending`, poll `client.calls.get(call_id)` to watch the status advance. Don't assume the call is connected the moment `create()` returns. ## Inbound: what happens when someone calls your number Inbound is simpler - there is no queue, because there is nothing to rate-limit. The moment a caller dials a number attached to your Speech Pipe, the call is already live and Unpod connects your agent to it. The carrier delivers the call over SIP. LiveKit answers, creates a room, and adds the caller as a participant. Unpod matches the dialed number to your Speech Pipe and its voice profile. A Unpod media worker joins the room the caller is already in and bridges the audio; transcripts start streaming to your `AgentRunner` over its text bridge immediately. ## The audio + transcript path Once both legs are in the room, every call works the same way. Unpod's speech stack transcribes the caller and streams plain **text** to your `AgentRunner`; your dialog logic replies with text; Unpod synthesizes it back to speech. The animated lifecycle above is the same loop in motion: call audio stays on Unpod's side, text crosses into your runner, and reply text comes back for TTS. Your `agent_id` selects **which dialog brain** runs for the call - it is not how Unpod routes the phone number. Number routing is handled by the [Speech Pipe](/get-started/core-concepts#pipe) the number is attached to. ## Voicemail detection Unpod automatically detects when an outbound call has reached a voicemail system so your agent does not waste a turn talking to a machine. There are two detection points: Many carriers route "forwarded to voicemail" to a SIP *user-unavailable* signal after a brief ring. When Unpod sees this pattern, it classifies the call as voicemail **before** it ever connects, ends the call, and reports `end_reason: "VOICEMAIL_DETECTED_PRECONNECT"`. Some voicemail systems answer, play a greeting, then go silent. If a call connects, the caller never speaks, and the leg ends within a short window, Unpod classifies it as voicemail and reports `end_reason: "AGENT_HUNG_UP_VOICEMAIL_DETECTED"`. ## End reasons When a call finishes, Unpod records why. These are the reasons you are most likely to act on: | `end_reason` | Meaning | | ---------------------------------- | -------------------------------------------------------- | | `USER_HUNG_UP_IN_CALL` | The caller hung up during the conversation. | | `USER_DID_NOT_PICK_UP` | Outbound call rang out with no answer. | | `USER_HUNG_UP_RINGING` | The caller rejected the call while ringing. | | `AGENT_HUNG_UP_SILENCE_DETECTED` | The agent ended the call after prolonged user silence. | | `AGENT_HUNG_UP_VOICEMAIL_DETECTED` | Voicemail detected after connect; agent hung up. | | `VOICEMAIL_DETECTED_PRECONNECT` | Voicemail detected during ringing; call never connected. | | `MAX_DURATION_REACHED` | The hard per-call duration cap was hit. | | `IDLE_TIMEOUT` | The call was idle too long and was reaped. | | `SIP_FAILED_WRONG_NUMBER` | The number was invalid or unreachable. | | `SIP_FAILED_NUMBER_BLOCKED` | The carrier blocked the call. | This is not the full set - additional reasons exist for SIP/trunk configuration errors and handover edge cases. Treat unknown reasons as a generic failure and log them. ## Reacting to outcomes in your agent Observe telephony outcomes through hooks rather than polling: ```python theme={null} @ctx.session.on("call_end") async def on_end(final_state: str) -> None: reason = ctx.session.data.get("end_reason") if reason == "VOICEMAIL_DETECTED_PRECONNECT": await schedule_retry(ctx.user_number, after_minutes=120) elif reason in ("SIP_FAILED_WRONG_NUMBER", "SIP_FAILED_NUMBER_BLOCKED"): await mark_unreachable(ctx.user_number) ``` ## Related * [Outbound Calls](/speech-stack/outbound-calls) - the `calls.create()` API in detail * [Speech Pipes](/speech-stack/pipes) - what ties a number, voice profile, and agent together * [Hooks & Events](/speech-stack/hooks-events) - every lifecycle event you can hook * [AgentRunner & Sessions](/speech-stack/agent-runner) - ending and transferring live calls # Deploy Source: https://docs.unpod.ai/speech-stack/deploy Run AgentRunner in production: reachability, authentication, scaling, and shutdown. An `AgentRunner` is a long-lived process: it registers with the Unpod orchestrator over WSS and heartbeats (at an interval the orchestrator assigns on registration). Under the default `dial_out` transport it then **dials out** to a per-call bridge each time a call is assigned; under the legacy `serve` transport it instead hosts a bridge connection per call. Provisioning is covered by the [Provisioning checklist](/speech-stack/setup-checklist); this page is what changes when you leave your laptop. Since v2 the runner defaults to the `dial_out` transport: both the control connection and the per-call bridge are **outbound** from the runner, so a runner behind NAT needs no public reachability, `serving_url`, or `agent_secret`. The reachability and authentication steps below apply only when you opt into the legacy `transport="serve"` model. See [AgentRunner constructor](/speech-stack/agent-runner#constructor). ## What production needs A production deployment should have: * A stable `UNPOD_API_KEY` - **required by the `AgentRunner`** for the orchestrator connection ([full variable table](/speech-stack/setup-checklist#environment-variables)) * A clear `agent_id` shared by every replica of the same agent * Capacity limits (`max_sessions`, `permits_per_minute`) that match your traffic * Shutdown handling so active calls can drain before the process exits * Observability for active calls, failures, and latency * **(legacy `serve` transport only)** a public `serving_url` so Unpod can reach the runner bridge, and an `agent_secret` so inbound bridge connections are signed and verified If you are writing docs for app developers, this is the minimum contract: 1. Set the environment variables. 2. Run multiple replicas with the same `agent_id` when you need scale. 3. Restart the process under a supervisor. 4. **(legacy `serve` transport only)** expose the runner publicly and lock down the bridge with `UNPOD_AGENT_SECRET`. ## Production checklist ### 1. Reachability - `serving_url` (legacy `serve` transport only) This step applies only to `transport="serve"`. Under the default `dial_out` transport the runner never listens - it dials out to the bridge URL delivered in each `job.assign` frame - so no public reachability is needed. In `serve` mode the runner hosts a per-call bridge that Unpod dials into. It binds to `0.0.0.0:8765` by default; in production, tell the orchestrator where that bridge is publicly reachable: ```bash theme={null} export UNPOD_RUNNER_URL="wss://agents.example.com:8765" ``` (or pass `serving_url=...` to `AgentRunner`). The host/port must be reachable from Unpod. ### 2. Authentication - `agent_secret` (legacy `serve` transport only) Also `serve`-only. Under the default `dial_out` transport the per-call bridge is authenticated by the per-call token embedded in the `job.assign` `bridge_url`, so `agent_secret` is ignored (the constructor warns if you pass it). In `serve` mode, with an agent secret set, every inbound bridge connection is HMAC-verified (signed URLs, replay-protected). Without one, the runner accepts unsigned connections - acceptable only for local dev. ```bash theme={null} export UNPOD_AGENT_SECRET="a-long-random-secret" ``` ### 3. Capacity ```python theme={null} AgentRunner( entrypoint=handle_call, agent_id="my-agent", max_sessions=50, # orchestrator never dispatches beyond this permits_per_minute=120, # rate of new call acceptance drain_timeout_s=60, ) ``` ### 4. Scaling - add replicas Run more processes with the **same `agent_id`** - on one machine or many. They form a pool; the orchestrator load-balances dispatches across replicas by reported capacity. No shared state, no coordination needed. ```bash theme={null} # replica 1, replica 2, ... identical: UNPOD_API_KEY=sk_... python agent.py ``` ### 5. Shutdown and supervision On `SIGTERM` the runner stops accepting dispatches and drains active calls for up to `drain_timeout_s` before exiting - container- and systemd-friendly. Under the default `dial_out` transport the runner **auto-reconnects** its orchestrator control socket with jittered exponential backoff (1s → 30s cap) and re-registers under the same `worker_id`; in-flight calls ride their own bridge sockets and survive a control-socket drop. Still run it under a supervisor with restart-on-exit (systemd `Restart=always`, Kubernetes restart policy, Docker `--restart`) to recover from process crashes and from a non-retriable transport rejection (an orchestrator too old to acknowledge `dial_out`). The legacy `transport="serve"` model does **not** auto-reconnect. ## Environment summary The minimum production `.env` - every variable defined in the [Provisioning checklist](/speech-stack/setup-checklist#environment-variables): ```bash theme={null} UNPOD_API_KEY="sk_..." # required by the AgentRunner UNPOD_BASE_URL="api.unpod.ai" UNPOD_PLATFORM_TOKEN="..." # management client; falls back to UNPOD_API_KEY UNPOD_ORG_HANDLE="acme" # org-scoped / telephony calls # Legacy transport="serve" only (ignored under the default dial_out transport): UNPOD_AGENT_SECRET="long-random-secret" UNPOD_RUNNER_URL="wss://agents.example.com:8765" ``` Constructor arguments beat the environment: * `AgentRunner(base_url=...)` overrides `UNPOD_ORCHESTRATOR_URL` * `AgentRunner(serving_url=...)` overrides `UNPOD_RUNNER_URL` * `AsyncClient(base_url=...)` overrides `UNPOD_SERVICE_BASE_URL` ## Watch it run Wire up [metrics and runner stats](/speech-stack/observability) before you need them. # Hooks & Events Source: https://docs.unpod.ai/speech-stack/hooks-events React to every call lifecycle event - turns, interruptions, silences, tool calls, and more. ## Overview The hook system lets you register `async` handlers that fire at specific points in a call's lifecycle. There are two levels of hooks: * **Runner-level** - fire for every call the runner handles (e.g. logging, metrics) * **Session-level** - fire on events within a single call (e.g. per-turn logic, silence detection) *** ## Registering Hooks ### Decorator style ```python theme={null} from unpod import AgentRunner, CallContext runner = AgentRunner(entrypoint=handle_call, agent_id="my-agent") # Runner-level hook @runner.on("call_start") async def on_any_call_start(ctx: CallContext) -> None: print(f"Runner received call: {ctx.call_id}") async def handle_call(ctx: CallContext) -> None: # Session-level hook - registered inside the entrypoint @ctx.session.on("user_turn") async def on_user_turn(text: str) -> None: print(f"User said: {text}") await ctx.session.run() ``` *** ## Runner-Level Events These fire at the runner level - once per call, regardless of what happens inside the session. | Event | Arguments | When | | ------------ | ------------------------------------ | ------------------------------------ | | `call_start` | `ctx: CallContext` | Call dispatched and bridge connected | | `call_end` | `ctx: CallContext, final_state: str` | Call finished (any reason) | `final_state` values: `"ended"`, `"failed"` ```python theme={null} @runner.on("call_start") async def log_call_start(ctx: CallContext) -> None: await db.insert_call_record( call_id=ctx.call_id, agent_id=ctx.agent_id, caller=ctx.user_number, direction=ctx.direction, ) @runner.on("call_end") async def log_call_end(ctx: CallContext, final_state: str) -> None: await db.update_call_record(ctx.call_id, final_state=final_state) ``` *** ## Session-Level Events Registered with `@ctx.session.on(event)` inside your entrypoint. These fire during `session.run()`. ### `call_start` Fires at the very beginning of the session loop, before any events are processed: ```python theme={null} @ctx.session.on("call_start") async def on_call_start() -> None: customer = await crm.lookup(ctx.user_number) ctx.session.data["customer"] = customer await ctx.session.say(f"Hello {customer['first_name']}, how can I help?") ``` ### `user_turn` Fires when the user has finished speaking and the transcription is ready: ```python theme={null} @ctx.session.on("user_turn") async def on_user_turn(text: str) -> None: await analytics.log_utterance( call_id=ctx.call_id, speaker="user", text=text, ) ``` ### `agent_turn` Fires after the agent's full reply has been generated (post-streaming): ```python theme={null} @ctx.session.on("agent_turn") async def on_agent_turn(text: str) -> None: await analytics.log_utterance( call_id=ctx.call_id, speaker="agent", text=text, ) ``` ### `interruption` Fires when the user interrupts the agent mid-utterance: ```python theme={null} @ctx.session.on("interruption") async def on_interruption() -> None: await analytics.log_event(ctx.call_id, "interruption") ``` ### `call_end` Fires when the session loop exits (call hung up, error, or `session.end()` called): ```python theme={null} @ctx.session.on("call_end") async def on_call_end(final_state: str) -> None: metrics = ctx.session.metrics.live() await db.save_call_summary( call_id=ctx.call_id, turns=metrics.turns, final_state=final_state, ) ``` *** ## Complete Hook Reference | Scope | Event | Arguments | Description | | ------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | Runner | `call_start` | `ctx: CallContext` | Call dispatched to this runner | | Runner | `call_end` | `ctx: CallContext, state: str` | Call finished - `state` is `"ended"` or `"failed"` | | Session | `call_start` | *(none)* | Session loop started | | Session | `user_turn` | `text: str` | User utterance transcribed | | Session | `user_partial` | `text: str` | Interim (partial) transcription | | Session | `agent_turn` | `text: str` | Full agent reply generated | | Session | `interruption` | *(none)* | User interrupted agent | | Session | `state` | `event: StateEvent` | Dialog state transition | | Session | `metric` | `event: MetricEvent` | Metric emitted | | Session | `llm_call` | `turn_id, node_id, model, call_type, latency_ms, tokens_in, tokens_out, prompt_messages, response_json, edge_id` | One LLM call within a turn (timing + tokens) | | Session | `turn_complete` | `turn_id, ttfa_ms, asr_ms, llm_ttft_ms, tts_ttfb_ms, stt_ms, tts_ms, from_node, to_node, llm_call_count, llm_total_ms, user_text, agent_text` | Turn finished, full stage latencies | | Session | `tool_call` / `tool_result` / `silence` | *(varies)* | Also registrable | | Session | `error` | `code, message, severity, source` | Session-level error | | Session | `call_end` | `state: str` | Session loop exited - `state` is `"hangup"` or `"error"` (distinct from the runner-level `"ended"`/`"failed"`) | *** ## Multiple Hooks for the Same Event You can register multiple handlers for the same event - all are called in registration order: ```python theme={null} @ctx.session.on("user_turn") async def log_turn(text: str) -> None: await logger.log(text) @ctx.session.on("user_turn") async def check_escalation(text: str) -> None: if "speak to a human" in text.lower(): await ctx.session.transfer_to_human(queue="support") ``` *** ## Practical Patterns ### Silence detection with timeout ```python theme={null} import asyncio async def handle_call(ctx: CallContext) -> None: last_activity = asyncio.get_event_loop().time() @ctx.session.on("user_turn") async def on_user_turn(text: str) -> None: nonlocal last_activity last_activity = asyncio.get_event_loop().time() @ctx.session.on("agent_turn") async def on_agent_turn(text: str) -> None: nonlocal last_activity last_activity = asyncio.get_event_loop().time() async def silence_watchdog() -> None: while True: await asyncio.sleep(5) idle = asyncio.get_event_loop().time() - last_activity if idle > 30: await ctx.session.say("Are you still there?") if idle > 60: await ctx.session.end(reason="no_response") break ctx.session.dialog_machine = my_machine await asyncio.gather( ctx.session.run(), silence_watchdog(), ) ``` ### CRM enrichment at call start ```python theme={null} async def handle_call(ctx: CallContext) -> None: @ctx.session.on("call_start") async def enrich() -> None: account = await crm.get_by_phone(ctx.user_number) if account: ctx.session.data["account"] = account ctx.session.dialog_machine.assist( f"Caller is {account['name']}, a {account['tier']} customer. " f"Their open ticket count is {account['open_tickets']}." ) ctx.session.dialog_machine = DialogMachine("support.yaml", llm="...") await ctx.session.run() ``` ### Escalation trigger ```python theme={null} async def handle_call(ctx: CallContext) -> None: @ctx.session.on("user_turn") async def detect_escalation(text: str) -> None: triggers = ["speak to a human", "real person", "your manager", "supervisor"] if any(t in text.lower() for t in triggers): await ctx.session.say( "Of course, let me transfer you to one of our team members." ) await ctx.session.transfer_to_human(queue="tier-1") ctx.session.dialog_machine = DialogMachine("support.yaml", llm="...") await ctx.session.run() ``` ### Full call logging ```python theme={null} import time from unpod import AgentRunner, CallContext runner = AgentRunner(entrypoint=handle_call, agent_id="my-agent") @runner.on("call_start") async def runner_start(ctx: CallContext) -> None: await db.create_call( call_id=ctx.call_id, session_id=ctx.session_id, direction=ctx.direction, caller=ctx.user_number, started_at=time.time(), ) @runner.on("call_end") async def runner_end(ctx: CallContext, final_state: str) -> None: m = ctx.session.metrics.live() await db.update_call( ctx.call_id, final_state=final_state, turns=m.turns, tokens_in=m.tokens.input, tokens_out=m.tokens.output, ) async def handle_call(ctx: CallContext) -> None: transcript: list[dict] = [] @ctx.session.on("user_turn") async def capture_user(text: str) -> None: transcript.append({"speaker": "user", "text": text}) @ctx.session.on("agent_turn") async def capture_agent(text: str) -> None: transcript.append({"speaker": "agent", "text": text}) @ctx.session.on("call_end") async def save_transcript(state: str) -> None: await db.save_transcript(ctx.call_id, transcript) ctx.session.dialog_machine = DialogMachine("support.yaml", llm="...") await ctx.session.run() ``` *** ## Next Steps Initiate calls programmatically from your backend. Use say, transfer, end, and recording controls. # How a call flows Source: https://docs.unpod.ai/speech-stack/introduction What runs between the caller audio and your agent text - whether users dial a number or connect from a browser or app. ## What runs the call Unpod gives your agent voice. It handles everything between the user's audio and your agent's text logic. Your agent receives plain text. It returns plain text. The Speech Stack handles everything else: transcription, synthesis, VAD, barge-in detection, endpointing, and transport. *** ## Two Ways Users Connect Users dial a phone number. Unpod handles SIP, PSTN, number provisioning, and routing. No carrier account or SIP trunk needed. Users connect from a web app or mobile app via the Unpod Web SDK. Same speech pipeline - no phone number required. Both paths run through the same managed speech pipeline (STT, TTS, VAD, barge-in). Your agent code is identical regardless of how the user connects. *** ## How Audio Flows Unpod voice stack diagram showing phone, browser, and mobile entrypoints flowing through the managed speech layer into your AgentRunner and dialog machine. Via a phone number (PSTN/SIP) or directly from a browser/app using the Web SDK. Unpod transcribes audio (STT), detects turn end (VAD + endpointing), and handles barge-in interruptions. Your agent gets clean text. The Unpod orchestrator dispatches the session to your `AgentRunner`. Your entrypoint runs, your dialog machine produces a text reply. Unpod converts the reply to speech (TTS) and streams it back to the user. Transcript, metrics, and recording are stored and queryable via the management API. *** ## Core Building Blocks Choose STT and TTS providers. Pre-built profiles or custom combinations with automatic failover. Bundle a voice profile, recording settings, and connection attachments, then point the pipe at your agent. Provision numbers directly or bring your own. Attach to a Speech Pipe for inbound calls. Install `unpod`, run your `AgentRunner`, and accept sessions from any source. *** ## Quickstart Paths | I want to... | Start here | | ---------------------- | ------------------------------------------------------------------------------------------------------------ | | Handle phone calls | [Telephony Quickstart](/get-started/first-phone-call) - provision a number and attach it to your Speech Pipe | | Add voice to a web app | [Web SDK Quickstart](/get-started/quickstart) - embed the Unpod Web SDK in your frontend | | Connect both | Start with telephony, then add the Web SDK - same Speech Pipe, two entry points | *** ## Next Steps End-to-end: agent running and accepting sessions in under 10 minutes. Drive conversations with structured flows and tools. # Run a SuperDialog agent Source: https://docs.unpod.ai/speech-stack/level-up-superdialog When a plain LLM brain stops being enough: plug a structured SuperDialog agent into your voice agent. A plain LLM brain answers questions. It does not reliably follow a multi-step process, collect required fields in order, branch on conditions, or call your tools at the right moment. When your voice agent needs that - appointment booking, triage, verification - level up to [SuperDialog](/superdialog/introduction): a runtime that executes a **playbook** (journeys of checkpoints that gate outcomes) turn by turn. Wiring is one assignment - the SDK auto-wraps it in a `SuperDialogAdapter` (see [Adapters](/speech-stack/adapters#auto-wrapping)): ```python theme={null} from superdialog import DialogMachine from unpod import AgentRunner, CallContext async def handle_call(ctx: CallContext) -> None: ctx.session.dialog_machine = DialogMachine( "clinic.yaml", # any format; Playbook engine by default llm="anthropic/claude-haiku-4-5", ) await ctx.session.run() # hands every turn to the agent AgentRunner(entrypoint=handle_call, agent_id="my-agent").start() ``` The runnable end-to-end version - Speech Pipe registration, pre-call data, mid-call `assist()`, flow switching - lives in [Embedding guide: Unpod voice](/superdialog/embedding-guides/unpod-voice). ## Voice-specific patterns Two things you only hit inside a live call. ### Give tools call context Define tools as closures inside your entrypoint so they capture per-call data: ```python theme={null} from superdialog.tools import tool async def handle_call(ctx: CallContext) -> None: caller_number = ctx.user_number @tool def lookup_caller() -> dict: """Look up the caller's account by phone number.""" return crm.lookup_phone(caller_number) # closure over call data ctx.session.dialog_machine = DialogMachine( "clinic.yaml", llm="anthropic/claude-haiku-4-5", tools=[lookup_caller] ) await ctx.session.run() ``` ### Detect completion Both engines have terminal states. After `run()` returns, check whether the dialog finished or the caller hung up mid-conversation: ```python theme={null} async def handle_call(ctx: CallContext) -> None: machine = DialogMachine("clinic.yaml", llm="anthropic/claude-haiku-4-5") ctx.session.dialog_machine = machine await ctx.session.run() if machine.is_complete: print("Dialog reached a terminal checkpoint") else: print("Call ended mid-conversation (hang up / timeout)") ``` ## Authoring the playbook Generate one with `superdialog generate`, version-control the YAML, and iterate with `superdialog chat` - no voice setup needed ([Quickstart](/superdialog/quickstart)). The YAML vocabulary and field tables are in [Playbooks](/superdialog/playbooks); HTTP, Python, and MCP tools are in [SuperDialog Tools](/superdialog/tools). ## Go deeper The full worked example: a SuperDialog agent inside an AgentRunner session. The mental model: checkpoints that gate outcomes, and when not to use one. The framework itself: playbooks, tools, sessions, CLI. Prefer your own brain? Adapters for LangChain, OpenAI, Anthropic, HTTP. # Phone Numbers Source: https://docs.unpod.ai/speech-stack/numbers Get numbers directly from Unpod or bring your own, then attach them to your Speech Pipes. ## Overview A [Number](/get-started/core-concepts#number) is the phone number callers dial; it routes to whatever Speech Pipe it is attached to. Phone numbers in Unpod come from two sources: * **Unpod Numbers** (default) - provision directly from the Unpod platform. No carrier account, no [trunk](#trunks), no SIP. * **BYON (Bring Your Own Number)** - route numbers you already own from any provider over a [trunk](#trunks) you register once. Once a number is in your account, you attach it to a Speech Pipe. All inbound calls to that number are routed to the Speech Pipe's `AgentRunner`. Support for Twilio, Telnyx, and Plivo numbers is coming soon - you will be able to import numbers from these providers directly into Unpod. *** ## Number Sources | Source | Description | | ---------- | -------------------------------------------------- | | **Unpod** | Provision numbers directly from the Unpod platform | | **BYON** | Bring numbers from your existing provider | | **Twilio** | Coming soon | | **Telnyx** | Coming soon | | **Plivo** | Coming soon | *** ## Getting Numbers from Unpod Numbers provisioned through Unpod are immediately available in your account. You can browse and acquire them from the dashboard under **Dev Platform -> Numbers**. Once provisioned, list them via the SDK: ```python theme={null} import asyncio from unpod import AsyncClient async def main(): async with AsyncClient() as client: # reads UNPOD_API_KEY numbers = await client.numbers.list() for n in numbers: print(n.number_id, n.number, n.status, n.pipe_id or "unattached") asyncio.run(main()) ``` *** ## Trunks A [Trunk](/get-started/core-concepts#trunk) is your carrier connection - the SIP capacity numbers ride on. Bring your own carrier (Twilio, Tata, …), register it once with its SIP credentials, then work with numbers, not SIP. BYON numbers arrive this way. **Dev Platform -> Telephony -> Add Trunk -> BYO SIP**. Provide the carrier's **SIP domain** (e.g. `sip.your-carrier.com`) and **auth username / password**. Set inbound routing to the Unpod SIP endpoint shown in the dashboard. Click **Sync** on the trunk, or run `await client.numbers.sync()`. The trunk's numbers appear in your account, ready to attach to a [Speech Pipe](/speech-stack/pipes). ### Register a trunk via the SDK Field names match the dashboard. ```python theme={null} from unpod import AsyncClient from unpod.models import TrunkCreate, ByoConfigCreate client = AsyncClient() trunk = await client.trunks.create(TrunkCreate( name="tata-byo", type="byo", byo_config=ByoConfigCreate( provider="tata", sip_domain="sip.tata.in", auth_username="user", auth_password="secret", transport="tls", # default ), )) ``` ### List and delete trunks ```python theme={null} trunks = await client.trunks.list() await client.trunks.delete(trunk.trunk_id) ``` ### Sync numbers off a trunk ```python theme={null} result = await client.numbers.sync() # {"synced": int, "new": int} ``` One-time provisioning end to end (profile, pipe, number, env vars): [Provisioning checklist](/speech-stack/setup-checklist). *** ## Listing Numbers ```python theme={null} import asyncio from unpod import AsyncClient async def main(): async with AsyncClient() as client: # reads UNPOD_API_KEY # All numbers numbers = await client.numbers.list() # Filter by status or country active = await client.numbers.list(status="active") us_numbers = await client.numbers.list(country="US") byon = await client.numbers.list(trunk_type="byo") for n in numbers: print(n.number_id, n.number, n.status, n.pipe_id or "unattached") asyncio.run(main()) ``` ### Number fields | Field | Type | Description | | ------------ | ------------- | --------------------------------------- | | `number_id` | `str` | Unique number ID (`num_...`) | | `number` | `str` | E.164 format, e.g. `+14155550100` | | `status` | `str` | `active`, `inactive`, `pending` | | `trunk_type` | `str` | `unpod` or `byo` | | `pipe_id` | `str \| None` | Attached Speech Pipe, or `None` if free | | `country` | `str` | ISO 3166-1 alpha-2 country code | *** ## Attaching a Number to a Speech Pipe A number can be attached to exactly one Speech Pipe at a time. Inbound calls to that number are routed to the Speech Pipe's configured runners. ```python theme={null} import asyncio from unpod import AsyncClient async def main(): async with AsyncClient() as client: # reads UNPOD_API_KEY number = await client.numbers.attach( number_id="num_...", pipe_id="pipe_...", ) print("Attached:", number.number, "-> pipe", number.pipe_id) asyncio.run(main()) ``` *** ## Detaching a Number ```python theme={null} import asyncio from unpod import AsyncClient async def main(): async with AsyncClient() as client: # reads UNPOD_API_KEY number = await client.numbers.detach("num_...") print("Detached:", number.number) asyncio.run(main()) ``` *** ## Common Patterns ### Find the first free number and attach it ```python theme={null} import asyncio from unpod import AsyncClient async def attach_first_free(pipe_id: str) -> None: async with AsyncClient() as client: # reads UNPOD_API_KEY numbers = await client.numbers.list(status="active") free = [n for n in numbers if n.pipe_id is None] if not free: raise RuntimeError("No free numbers available") await client.numbers.attach(number_id=free[0].number_id, pipe_id=pipe_id) print("Attached", free[0].number) asyncio.run(attach_first_free("pipe_...")) ``` ### Rotate numbers across Speech Pipes ```python theme={null} import asyncio from unpod import AsyncClient async def rotate(numbers: list[str], pipes: list[str]) -> None: async with AsyncClient() as client: # reads UNPOD_API_KEY for num_id, pipe_id in zip(numbers, pipes): await client.numbers.attach(number_id=num_id, pipe_id=pipe_id) print(f"{num_id} -> {pipe_id}") asyncio.run(rotate(["num_a", "num_b"], ["pipe_x", "pipe_y"])) ``` *** ## Next Steps Choose the voice profile for your Speech Pipe. Create and configure your Speech Pipe via the SDK. # Metrics, Cost & Observability Source: https://docs.unpod.ai/speech-stack/observability Per-turn timing & token hooks, the usage/billing ledger, Langfuse tracing, runner pool stats, and post-call timing. Layers of visibility, from inside a call out to your dashboards and billing. ## 1. Per-turn timing & token hooks The real per-turn timing and token numbers arrive on two hooks the core fires every turn. Register them on the session: ```python theme={null} @ctx.session.on("llm_call") async def _(turn_id, node_id, model, call_type, latency_ms, tokens_in, tokens_out, prompt_messages, response_json, edge_id) -> None: # fired once per LLM call within a turn push_to_grafana({"model": model, "latency_ms": latency_ms, "tokens_in": tokens_in, "tokens_out": tokens_out}) @ctx.session.on("turn_complete") async def _(turn_id, ttfa_ms, asr_ms, llm_ttft_ms, tts_ttfb_ms, stt_ms, tts_ms, from_node, to_node, llm_call_count, llm_total_ms, user_text, agent_text) -> None: # fired once per completed user->agent turn, with full stage latencies print(turn_id, ttfa_ms, llm_total_ms) ``` `llm_call` and `turn_complete` carry the actual per-turn latency and token data. The `CallMetrics` snapshot below (`metrics.live()`) only populates if you feed it from these hooks - see the caveat in Layer 2. ## 2. Live per-call snapshot (opt-in) Inside (or after) `session.run()`, take a `CallMetrics` snapshot: ```python theme={null} m = ctx.session.metrics.live() m.turns # int: dialog turns m.duration_s # float m.stt_p95_ms # int: P95 speech-to-text latency m.llm_p95_ms # int: P95 brain latency m.tts_p95_ms # int: P95 synthesis latency m.cost.total # float m.tokens.input # int m.tokens.output # int m.active_llm # str: model used on the last turn ``` The latency/cost/token fields populate **only if you call `metrics.record_turn(...)` yourself** - typically from the `turn_complete` hook above. The SDK does not auto-fill them, so out of the box only `duration_s` is meaningful. For real numbers, use the per-turn hooks (Layer 1) or the post-call transcript timing (Layer 5). ## 3. Usage & billing ledger The SDK buffers per-session LLM usage and flushes it to the cloud billing ledger on `call_end`. It is **best-effort**: a no-op when `UNPOD_USAGE_INGEST_URL` is unset, and never blocks or fails the call. ```bash theme={null} UNPOD_USAGE_INGEST_URL="https://.../ingest" # enables the ledger UNPOD_USAGE_INGEST_TOKEN="..." # optional auth ``` Counters posted per session: | Counter | Meaning | | ---------------------------- | ----------------------------- | | `llm_prompt_tokens` | Prompt tokens | | `llm_completion_tokens` | Completion tokens | | `llm_cached_tokens` | Prompt-cache **read** tokens | | `llm_cache_write_tokens` | Prompt-cache **write** tokens | | `llm_provider` / `llm_model` | Provider + model attribution | Prompt-cache read/write tokens are forwarded so cached turns are billed at the correct (lower) rate. ## 4. Langfuse tracing When `LANGFUSE_SECRET_KEY` is set, the SDK emits per-turn spans plus a generation span per LLM call (with token usage). No wiring needed - set the key and traces appear in Langfuse. When unset, tracing is a no-op. ```bash theme={null} LANGFUSE_SECRET_KEY="sk-lf-..." ``` ## 5. Runner pool stats ```python theme={null} s = runner.stats() # RunnerStats snapshot s.in_flight # current active calls s.queued # dispatches waiting for capacity s.capacity # your max_sessions setting s.completed_last_hour s.failed_last_hour s.mean_call_duration_s ``` Poll this on a timer for liveness dashboards - see [AgentRunner & Sessions](/speech-stack/agent-runner#running-in-production). ## 6. Post-call timing After the call, the transcript carries a per-turn, per-stage latency breakdown (`audio_ingress_ms`, `stt_ms`, `bridge_to_dev_ms`, `dev_brain_ms`, `tts_ms`) - see [Recordings & Transcripts](/speech-stack/recordings-transcripts). High `dev_brain_ms` with healthy `stt_ms`/`tts_ms` means the latency is in YOUR brain - usually a `stream()` that is not actually streaming. See [Streaming is the hot path](/speech-stack/bring-your-agent#streaming-is-the-hot-path). # Outbound Calls Source: https://docs.unpod.ai/speech-stack/outbound-calls Initiate calls programmatically from your backend - campaigns, callbacks, and reminders. ## Overview Outbound calls are calls your system initiates - rather than waiting for a caller to ring in. Common use cases: * Appointment reminders * Sales outreach campaigns * Callback queues * Alert notifications Outbound calls use the same Speech Pipe, voice profile, and `AgentRunner` infrastructure as inbound calls. The difference is you trigger them via the management API. *** ## Prerequisites * A configured Speech Pipe with a voice profile - see [Speech Pipe](/speech-stack/pipes) * A phone number attached to that Speech Pipe - see [Numbers](/speech-stack/numbers) * A running `AgentRunner` process for the Speech Pipe *** ## Initiating an Outbound Call ```python theme={null} import asyncio from unpod import AsyncClient async def main(): async with AsyncClient() as client: # reads UNPOD_API_KEY call = await client.calls.create( pipe_id="pipe_...", to_number="+14155550100", # E.164 format from_number="+18005551234", # optional if a number is attached to the Speech Pipe instructions="This is a reminder call for a dental appointment.", data={ "patient_name": "Jane Smith", "appointment_date": "2026-06-10", "appointment_time": "09:30 AM", "doctor": "Dr. Patel", }, ) print("Call initiated:", call.call_id, call.status) # status is "pending" - calls.create() enqueues the call and returns # immediately. The call is dispatched asynchronously once the account # has free concurrency. Poll GET /calls/{id} (or use hooks) to watch # it advance to ringing -> active -> completed. See Call Lifecycle. asyncio.run(main()) ``` `calls.create()` is asynchronous: it enqueues the call and returns a record with `status="pending"`. The call is dispatched on a worker as soon as your account has free concurrency. See [Call Lifecycle](/speech-stack/call-lifecycle) for the full status progression and how to poll for completion. ### `calls.create()` parameters | Parameter | Type | Required | Description | | -------------- | -------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `to_number` | `str` | Yes | Destination number (E.164) | | `agent_id` | `str \| None` | Conditional | Agent to route the call to. Takes priority over `pipe_id`; when set, the platform resolves the pipe bound to that agent server-side | | `pipe_id` | `str \| None` | Conditional | Speech Pipe to use. Provide `agent_id` **or** `pipe_id` (`agent_id` wins if both are given) | | `from_number` | `str` | No | Caller ID. If omitted, Unpod uses a number attached to the Speech Pipe | | `instructions` | `str \| None` | No | Runtime instruction override for this call | | `data` | `dict \| None` | No | Arbitrary metadata passed to your entrypoint via `ctx.data` | *** ## Accessing Outbound Data in Your Entrypoint The `data` and `instructions` you pass to `calls.create()` are available on the `CallContext`: ```python theme={null} from superdialog import DialogMachine from unpod import AgentRunner, CallContext async def handle_call(ctx: CallContext) -> None: # Available for both inbound and outbound calls print(ctx.direction) # "outbound" print(ctx.instructions) # "This is a reminder call..." print(ctx.data) # {"patient_name": "Jane Smith", ...} # Inject context into the dialog machine patient_name = ctx.data.get("patient_name", "there") appt_date = ctx.data.get("appointment_date", "soon") appt_time = ctx.data.get("appointment_time", "") ctx.session.dialog_machine = DialogMachine( "reminder.yaml", llm="anthropic/claude-haiku-4-5", ) # Inject the personalization as an assist directive ctx.session.dialog_machine.assist( f"You are calling {patient_name}. " f"Their appointment is on {appt_date} at {appt_time}. " f"Confirm they will attend, or reschedule if needed." ) await ctx.session.run() runner = AgentRunner( entrypoint=handle_call, agent_id="my-agent", ) runner.start() ``` *** ## Checking Call Status ```python theme={null} import asyncio from unpod import AsyncClient async def main(): async with AsyncClient() as client: # reads UNPOD_API_KEY call = await client.calls.get("cal_...") print(call.call_id, call.status, call.direction) # status: "pending", "ringing", "active", "completed", "failed", "cancelled" asyncio.run(main()) ``` ### Call fields | Field | Type | Description | | ------------- | --------------- | ----------------------------------------------------------------------------------- | | `call_id` | `str` | Unique call ID (`cal_...`) | | `status` | `str` | `pending`, `ringing`, `active`, `completed`, `failed`, `cancelled` | | `direction` | `str` | `inbound` or `outbound` | | `to_number` | `str` | Destination number (E.164) | | `from_number` | `str \| None` | Caller ID used for the call | | `pipe_id` | `str` | Speech Pipe that handled the call | | `duration_s` | `float \| None` | Call duration in seconds (set once completed) | | `end_reason` | `str \| None` | Why the call ended - see [Call Lifecycle](/speech-stack/call-lifecycle#end-reasons) | *** ## Listing Calls ```python theme={null} import asyncio from unpod import AsyncClient async def main(): async with AsyncClient() as client: # reads UNPOD_API_KEY # All outbound calls for a Speech Pipe calls = await client.calls.list( pipe_id="pipe_...", status="completed", ) for c in calls: print(c.call_id, c.to_number, c.status, c.duration_s) asyncio.run(main()) ``` ### `calls.list()` filters | Parameter | Values | Description | | --------- | ------------------------------------------------------------------ | --------------------- | | `status` | `pending`, `ringing`, `active`, `completed`, `failed`, `cancelled` | Filter by status | | `pipe_id` | `pipe_...` | Filter by Speech Pipe | *** ## Hanging Up an Active Call ```python theme={null} import asyncio from unpod import AsyncClient async def main(): async with AsyncClient() as client: # reads UNPOD_API_KEY await client.calls.hangup("cal_...") print("Call terminated") asyncio.run(main()) ``` *** ## Campaign Pattern: Batch Outbound ```python theme={null} import asyncio from unpod import AsyncClient PIPE_ID = "pipe_..." FROM_NUMBER = "+18005551234" contacts = [ {"name": "Alice", "phone": "+14155550101", "appt": "June 10 9:30 AM"}, {"name": "Bob", "phone": "+14155550102", "appt": "June 10 2:00 PM"}, {"name": "Carol", "phone": "+14155550103", "appt": "June 11 10:00 AM"}, ] async def dial_all(contacts: list[dict]) -> None: async with AsyncClient() as client: # reads UNPOD_API_KEY for contact in contacts: call = await client.calls.create( pipe_id=PIPE_ID, to_number=contact["phone"], from_number=FROM_NUMBER, instructions=( f"Call {contact['name']} to confirm their appointment " f"on {contact['appt']}." ), data=contact, ) print(f"Dialing {contact['name']}: {call.call_id}") await asyncio.sleep(2) # pace the campaign asyncio.run(dial_all(contacts)) ``` Batch dialing at high rates may trigger carrier spam filters. Use a sensible interval between calls and comply with local regulations (TCPA in the US, etc.). *** ## Callback Queue Pattern ```python theme={null} import asyncio from unpod import AsyncClient async def process_callback_queue(queue: list[dict]) -> None: async with AsyncClient() as client: # reads UNPOD_API_KEY for request in queue: # Check if the Speech Pipe is not at capacity before dialing # (AgentRunner handles capacity internally - orchestrator rejects if full) call = await client.calls.create( pipe_id="pipe_...", to_number=request["phone"], from_number="+18005551234", instructions="This is a callback. The customer requested to be called back.", data={"ticket_id": request["ticket_id"], "reason": request["reason"]}, ) await db.update_callback_request( request["id"], call_id=call.call_id, status="dialing", ) ``` *** ## Retrieving Transcripts After a Call ```python theme={null} import asyncio from unpod import AsyncClient async def get_transcript(session_id: str) -> None: async with AsyncClient() as client: # reads UNPOD_API_KEY session = await client.transcripts.get(session_id) for entry in session.transcript: print(f"[{entry.role}] {entry.content}") asyncio.run(get_transcript("sess_...")) ``` *** ## Next Steps Log outbound call data and react to call events in real time. Configure Speech Pipes and voice profiles for outbound campaigns. # Speech Pipe Source: https://docs.unpod.ai/speech-stack/pipes Create, configure, and manage Speech Pipes via the Unpod SDK. ## What Is a Speech Pipe? A **[Speech Pipe](/get-started/core-concepts#pipe)** in Unpod ties together a voice profile (STT + TTS), telephony (phone numbers), and a pointer to your agent brain into a single deployable unit. When a call arrives on a number attached to the pipe, the orchestrator dispatches it to your `AgentRunner` process. Animated Speech Pipe flow showing a phone number routing into a Speech Pipe, the voice profile connecting to the STT/TTS stack, and dispatch to AgentRunner and CallContext. *** ## Creating a Pipe ```python theme={null} import asyncio from unpod import AsyncClient async def main(): async with AsyncClient() as client: # reads UNPOD_API_KEY pipe = await client.pipes.create( name="Support Bot", voice_profile="vp_en_female_hd", # from voice_profiles.list() agent_id="my-bot", # links to your AgentRunner recording=True, # store call recordings max_call_duration_s=600, # 10-minute hard cap ) print("Created:", pipe.pipe_id, pipe.name) asyncio.run(main()) ``` ### `create()` parameters | Parameter | Type | Default | Description | | --------------------- | ------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `str` | required | Display name for the pipe | | `voice_profile` | `str \| None` | `None` | Voice profile ID (`vp_...`) | | `agent_id` | `str \| None` | `None` | ID of your `AgentRunner` brain - links the pipe to your runner worker (preferred) | | `agent_endpoint` | `str \| None` | `None` | Static **WebSocket** URL (`wss://...`) of your `AgentRunner` bridge - a fallback used when no runner has registered for `agent_id`. Not an HTTP URL. | | `recording` | `bool` | `False` | Enable call recording | | `max_call_duration_s` | `int` | `3600` | Hard cap in seconds (default 1 hour) | *** ## Listing Pipes ```python theme={null} import asyncio from unpod import AsyncClient async def main(): async with AsyncClient() as client: # reads UNPOD_API_KEY pipes = await client.pipes.list() for p in pipes: print(p.pipe_id, p.name, p.voice_profile_id or "no voice profile") asyncio.run(main()) ``` ### Pipe fields | Field | Type | Description | | --------------------- | ------------- | -------------------------------------------------------------------------- | | `pipe_id` | `str` | Speech Pipe ID (`pipe_...`) | | `name` | `str` | Display name | | `voice_profile_id` | `str \| None` | Voice profile ID | | `agent_id` | `str \| None` | Your `AgentRunner` brain ID | | `agent_endpoint` | `str \| None` | Static `wss://` URL of your `AgentRunner` bridge (fallback for `agent_id`) | | `recording` | `bool` | Recording enabled | | `max_call_duration_s` | `int` | Call duration hard cap | | `created` | `datetime` | Creation timestamp | | `modified` | `datetime` | Last modified timestamp | *** ## Getting a Single Pipe ```python theme={null} import asyncio from unpod import AsyncClient async def main(): async with AsyncClient() as client: # reads UNPOD_API_KEY pipe = await client.pipes.get("pipe_...") print(pipe.name, pipe.voice_profile_id, pipe.recording) asyncio.run(main()) ``` *** ## Updating a Pipe ```python theme={null} import asyncio from unpod import AsyncClient async def main(): async with AsyncClient() as client: # reads UNPOD_API_KEY # Update any subset of fields pipe = await client.pipes.update( "pipe_...", name="Premium Support Bot", voice_profile="vp_en_female_hd", recording=True, max_call_duration_s=1200, ) print("Updated:", pipe.pipe_id, pipe.name) asyncio.run(main()) ``` *** ## Deleting a Pipe Deleting a pipe does **not** automatically detach phone numbers. Detach all numbers before deleting to avoid orphaned routing. ```python theme={null} import asyncio from unpod import AsyncClient async def main(): async with AsyncClient() as client: # reads UNPOD_API_KEY # Detach numbers first numbers = await client.numbers.list() for n in numbers: if n.pipe_id == "pipe_...": await client.numbers.detach(n.number_id) # Then delete await client.pipes.delete("pipe_...") print("Deleted") asyncio.run(main()) ``` *** ## How the Pipe Reaches Your Brain The pipe never calls your brain over HTTP. On every call the orchestrator dispatches the session to a **worker**, and the worker connects to your `AgentRunner` over a **WebSocket bridge** (text in / text out). There are two ways the pipe finds that runner: 1. **By `agent_id` (preferred).** Run an `AgentRunner` with the same `agent_id` as the pipe. Under the default `dial_out` transport the runner never listens - it registers, then dials **out** to a per-call bridge when a call is assigned. The orchestrator picks a registered, least-loaded runner per call, and you never expose a URL, a tunnel, or a webhook. See [AgentRunner](/speech-stack/agent-runner). 2. **By `agent_endpoint` (legacy `serve` transport only).** Set the pipe's `agent_endpoint` to a fixed `wss://...` bridge URL that your runner serves. This applies only to `transport="serve"`, which is deprecated - under the default `dial_out` transport nothing listens on that URL. It is a **WebSocket** URL, not an HTTP endpoint. `agent_endpoint` is **not** an HTTP webhook - the orchestrator does not `POST` to it. It is the WebSocket bridge URL your `AgentRunner` serves on. ## Bridging a Remote HTTP Brain Already have a chatbot or API in any language? Run an `AgentRunner` whose dialog brain is an `HTTPAdapter`. It `POST`s each user turn to your endpoint from inside the runner - text in, text out, **not** OpenAI-compatible. Your endpoint keeps all LLM logic. ```python theme={null} from unpod import AgentRunner, CallContext from unpod.adapters.http import HTTPAdapter async def handle_call(ctx: CallContext) -> None: ctx.session.dialog_machine = HTTPAdapter( url="https://your-api.example.com/dialog/turn", ) await ctx.session.run() AgentRunner(entrypoint=handle_call, agent_id="my-bot").start() ``` ```jsonc theme={null} // POST https://your-api.example.com/dialog/turn { "text": "I'd like to reschedule my appointment", "context": { }, "session_id": "sess_xyz789", "system_instructions": ["..."] // only after assist() is called } // 200 response { "text": "Sure - what date works for you?" } ``` Streaming, `assist()`, and error surfaces: [Adapters](/speech-stack/adapters). *** ## Pipe + Runner Pattern (Recommended) The recommended pattern for production is a dedicated `AgentRunner` process: ```python theme={null} from unpod import AgentRunner, CallContext AGENT_ID = "my-bot" # must match the pipe's agent_id async def handle_call(ctx: CallContext) -> None: await ctx.session.say("Hello, how can I help you?") await ctx.session.run() runner = AgentRunner( entrypoint=handle_call, agent_id=AGENT_ID, max_sessions=20, ) runner.start() ``` The `agent_id` in `AgentRunner` must match the pipe's `agent_id` in the platform. The orchestrator uses it to route dispatches to the correct runner worker. *** ## Next Steps Configure the AgentRunner for production and act on live calls - say, transfer, end, record. # Recordings & Transcripts Source: https://docs.unpod.ai/speech-stack/recordings-transcripts Retrieve call audio and per-turn transcripts - with per-stage latency timing - after calls end. Every call can leave two artifacts: an audio **recording** (when the Speech Pipe has `recording=True`) and a turn-by-turn **transcript**. Both are retrieved through the Management API. ## Recordings ```python theme={null} from unpod import AsyncClient client = AsyncClient() sessions = await client.recordings.list() # all sessions = await client.recordings.list(call_id=cid) # one call ``` `recordings.list()` returns the **sessions** that have a recording; read the download URL off each one: ```python theme={null} s.session_id # str s.call_id # str | None s.duration_s # int | None s.recording_url # str | None - download / stream URL ``` Pause and resume recording during a live call (e.g. around card numbers) with `ctx.session.recording.pause(reason=...)` / `.resume()` - see [AgentRunner & Sessions](/speech-stack/agent-runner#recording-control). ## Transcripts ```python theme={null} sessions = await client.transcripts.list() # sessions that have a transcript session = await client.transcripts.get(session_id) ``` `transcripts.list()`/`.get()` return **sessions**; the turns live on `session.transcript`: ```python theme={null} for entry in session.transcript: print(entry.role, entry.content) # role: "agent" | "user" entry.timestamp # datetime | None ``` This is the post-call complement to the live metrics in [Observability](/speech-stack/observability). # Provisioning checklist Source: https://docs.unpod.ai/speech-stack/setup-checklist One-time resource provisioning before your first call. Before your first call routes to your Speech Pipe, complete these four steps once. ## 1. Pick a Voice Profile ```python theme={null} import asyncio from unpod import AsyncClient async def list_profiles(): client = AsyncClient() profiles = await client.voice_profiles.list() for p in profiles: print(p.profile_id, p.name) asyncio.run(list_profiles()) ``` Copy a `profile_id` (`vp_...`) for step 2. See [Voice Profiles](/speech-stack/voice-profiles) for filtering and fields. ## 2. Create a Speech Pipe ```python theme={null} import asyncio from unpod import AsyncClient async def create_pipe(): client = AsyncClient() pipe = await client.pipes.create( name="My Agent", voice_profile="vp_en_female_hd", # a profile_id from step 1 agent_id="my-agent", # MUST match AgentRunner(agent_id=...) ) print(pipe.pipe_id) # pipe_... - used for REST API calls return pipe asyncio.run(create_pipe()) ``` **`agent_id` vs `pipe.pipe_id`** * `pipe.pipe_id` - ID assigned by Unpod, used in REST API calls * `agent_id` - string name you choose; must exactly match `AgentRunner(agent_id=...)` These are different. Mismatching them is the most common first-run failure. Other pipe options (`recording`, `max_call_duration_s`, `agent_endpoint`) are in [Speech Pipe](/speech-stack/pipes). ## 3. Attach a Phone Number `attach()` takes the number's `number_id` (`num_...`) and the `pipe_id`. ```python theme={null} import asyncio from unpod import AsyncClient async def assign_number(pipe_id: str) -> None: client = AsyncClient() numbers = await client.numbers.list(status="active") free = [n for n in numbers if n.pipe_id is None] if not free: print("No free numbers. Provision one in the Unpod dashboard first.") return await client.numbers.attach(number_id=free[0].number_id, pipe_id=pipe_id) print(f"Attached {free[0].number}") asyncio.run(assign_number("pipe_...")) ``` No numbers yet, or bringing your own carrier? See [Phone Numbers](/speech-stack/numbers) and [Trunks](/speech-stack/numbers#trunks). ## 4. Start Your Runner ```python theme={null} from unpod import AgentRunner AgentRunner( entrypoint=entrypoint, agent_id="my-agent", # must match the pipe's agent_id above ).start() ``` ## Full Setup Script ```python theme={null} import asyncio from unpod import AsyncClient, AgentRunner, CallContext from unpod.adapters.langchain import LangChainAdapter async def setup(): client = AsyncClient() # 1. Pick a voice profile profiles = await client.voice_profiles.list() profile = profiles[0] # 2. Create the Speech Pipe pipe = await client.pipes.create( name="My Agent", voice_profile=profile.profile_id, agent_id="my-agent", ) # 3. Attach the first free number free = [n for n in await client.numbers.list() if n.pipe_id is None] if free: await client.numbers.attach(number_id=free[0].number_id, pipe_id=pipe.pipe_id) print(f"Speech Pipe created: {pipe.pipe_id}") print(f"Number: {free[0].number if free else 'none assigned'}") asyncio.run(setup()) ``` Then start your runner in a separate process: ```python theme={null} async def entrypoint(ctx: CallContext) -> None: ctx.session.dialog_machine = LangChainAdapter(your_chain) await ctx.session.run() AgentRunner(entrypoint=entrypoint, agent_id="my-agent").start() ``` ## Environment Variables | Variable | Required | Description | | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `UNPOD_API_KEY` | Yes | Direct-mode Bearer key. **Required by the `AgentRunner`** (orchestrator connection); also the management-client fallback when `UNPOD_PLATFORM_TOKEN` is unset | | `UNPOD_PLATFORM_TOKEN` | No | Backend-core DRF token - enables management **proxy mode** (preferred for the `AsyncClient`/`Client`). Falls back to `UNPOD_API_KEY` if unset | | `UNPOD_ORG_HANDLE` | No | Org handle sent as the `Org-Handle` header alongside `UNPOD_PLATFORM_TOKEN` (required for org-scoped / telephony endpoints) | | `UNPOD_BASE_URL` | No | Single shared endpoint. REST derives `https:///platform` and the runner derives `wss://` | | `UNPOD_SERVICE_BASE_URL` | No | Management REST override only. Takes precedence over `UNPOD_BASE_URL` when set | | `UNPOD_ORCHESTRATOR_URL` | No | Runner WebSocket override only. Takes precedence over `UNPOD_BASE_URL` when set | ### Which URL setting to use * `UNPOD_BASE_URL` - management API and runner share one host. * `UNPOD_SERVICE_BASE_URL` - management REST override only. * `UNPOD_ORCHESTRATOR_URL` - runner WebSocket override only. * `base_url=` / `orchestrator_base_url=` in code - one script or test overrides `.env`. Production values and the `serve`-transport extras: [Deploy](/speech-stack/deploy). # Voice Profiles Source: https://docs.unpod.ai/speech-stack/voice-profiles Browse and select the speech stack for your Speech Pipes. ## What Is a Voice Profile? A [Voice Profile](/get-started/core-concepts#voice-profile) bundles three things into a single reusable configuration: * **Speech-to-Text (STT)** - transcribes the caller's audio into text * **Text-to-Speech (TTS)** - converts agent text into natural-sounding audio * **Voice** - the specific voice identity (gender, accent, character) used for TTS output When you attach a voice profile to a Speech Pipe, every call that Speech Pipe handles uses that configuration automatically - including failover if a provider goes down. Voice profile pipeline diagram showing caller speech passing through STT, dialog machine text handling, TTS plus voice, and caller playback. Voice profiles are managed by Unpod. You browse and select from the available list - you do not configure STT/TTS providers directly. *** ## Listing Voice Profiles via SDK ```python theme={null} import asyncio from unpod import AsyncClient async def main(): async with AsyncClient() as client: # reads UNPOD_API_KEY # All profiles profiles = await client.voice_profiles.list() # Filter by language en_profiles = await client.voice_profiles.list(language="en") es_profiles = await client.voice_profiles.list(language="es") for p in profiles: print( p.profile_id, p.name, f"gender={p.gender}", f"quality={p.quality}", ) asyncio.run(main()) ``` ### VoiceProfile fields | Field | Type | Description | | ------------------ | ------------- | ---------------------------------------------- | | `profile_id` | `str` | Profile ID to pass when creating a Speech Pipe | | `name` | `str` | Human-readable name, e.g. `"Emma (HD)"` | | `gender` | `str` | `male`, `female`, or `neutral` | | `quality` | `str` | `standard` or `high` | | `languages` | `list[str]` | Supported language codes, e.g. `["en", "hi"]` | | `description` | `str \| None` | Short description of the voice character | | `latency_ms` | `int \| None` | Expected end-to-end latency in milliseconds | | `greeting_message` | `str \| None` | Default greeting for this profile | *** ## Getting a Single Profile ```python theme={null} import asyncio from unpod import AsyncClient async def main(): async with AsyncClient() as client: # reads UNPOD_API_KEY profile = await client.voice_profiles.get("vp_en_female_hd") print(profile.name, profile.quality, profile.languages) asyncio.run(main()) ``` *** ## Choosing the Right Profile ### By use case | Use case | Recommended qualities | | ------------------- | ---------------------------------------- | | Customer support | High quality, natural voice, low latency | | Sales outreach | Expressive, high quality, brand-matched | | Appointment booking | Standard quality - lower latency | | Multilingual | Match language codes to caller's region | ### By latency Total call latency has three components: STT transcription + LLM thinking + TTS synthesis. * Choose `quality == "high"` when voice naturalness matters more than speed * Choose `quality == "standard"` for latency-sensitive or high-volume deployments * Check `latency_ms` on the profile object for the estimated end-to-end figure *** ## Using a Profile When Creating a Speech Pipe Pass `profile_id` directly when creating or updating a Speech Pipe: ```python theme={null} import asyncio from unpod import AsyncClient async def main(): async with AsyncClient() as client: # reads UNPOD_API_KEY # List to find the right profile profiles = await client.voice_profiles.list(language="en") hd_female = next( p for p in profiles if p.quality == "high" and p.gender == "female" ) # Create Speech Pipe with that profile pipe = await client.pipes.create( name="Support Bot", voice_profile=hd_female.profile_id, ) print("Created Speech Pipe with profile:", hd_female.name) asyncio.run(main()) ``` *** ## Switching a Profile on an Existing Speech Pipe ```python theme={null} import asyncio from unpod import AsyncClient async def main(): async with AsyncClient() as client: # reads UNPOD_API_KEY pipe = await client.pipes.update( "pipe_...", voice_profile="vp_es_male_std", # switch to Spanish male ) print("Updated voice profile:", pipe.voice_profile_id) asyncio.run(main()) ``` *** ## Next Steps Attach a voice profile when creating or updating a Speech Pipe. Install the SDK and configure your runner process. # WebSocket Connectivity Source: https://docs.unpod.ai/speech-stack/websocket Unpod exposes your agent as a WebSocket endpoint. Any browser, app, or client connects to it. Speech is processed entirely by Unpod. ## How It Works When you create a Speech Pipe on Unpod, it is automatically available as a WebSocket endpoint. Any client - browser, mobile app, desktop, IoT device - can connect to that endpoint and have a real-time voice conversation. Your agent code does not change. Speech processing (STT, VAD, barge-in, endpointing, TTS) all happens on Unpod's side. Your AgentRunner just receives text and returns text, exactly as it does for phone calls. Animated WebSocket connectivity diagram showing browser, app, or client audio connecting through a pipe WSS endpoint into the Unpod speech pipeline, then text routing to your AgentRunner. *** ## Connecting a Client Unpod exposes a standard WebSocket URL per Speech Pipe. Your client connects with a short-lived token: `@unpod/web-sdk` is **not yet published on npm**. The snippet below is the intended shape, not something you can `npm install` today. To hear an agent in a browser now, use the [Playground](https://superdialog.unpod.ai/playground?tab=preview). ```typescript theme={null} import { UnpodSession } from "@unpod/web-sdk"; const session = new UnpodSession({ token: "", }); await session.connect(); // starts mic + speaker, full duplex audio session.on("agent_reply", (text) => console.log("Agent:", text)); session.on("user_turn", (text) => console.log("User:", text)); await session.disconnect(); ``` Clients can be anything that speaks WebSocket - browser SDK, mobile SDK, a raw WebSocket client, or a third-party integration. *** ## Generating a Session Token Your backend generates a short-lived token for each user session. Pass it to the client - never expose your API key on the frontend. ```python theme={null} from unpod import AsyncClient async def get_session_token(pipe_id: str, user_id: str) -> str: # reads UNPOD_API_KEY from the env; see the credential table in # /speech-stack/setup-checklist for UNPOD_PLATFORM_TOKEN + UNPOD_ORG_HANDLE async with AsyncClient() as client: token = await client.sessions.create_token( pipe_id=pipe_id, metadata={"user_id": user_id}, ) return token.token # single-use, expires in 60s ``` Return the token to your frontend via your own API. The client passes it to `UnpodSession`. *** ## Same Agent, Multiple Entry Points Your AgentRunner accepts sessions from all sources on the same `agent_id`. You do not need separate agents for phone calls and web/app clients. Animated Unpod voice stack diagram showing phone, browser, and mobile entrypoints reaching the same managed speech layer and AgentRunner. The `CallContext` tells you how the session arrived: ```python theme={null} from unpod import CallContext from superdialog import DialogMachine async def handle_call(ctx: CallContext) -> None: if ctx.direction == "inbound": print("caller dialled a number, or a client connected") elif ctx.direction == "outbound": print("your backend initiated the call") ctx.session.dialog_machine = DialogMachine("agent.yaml", llm="anthropic/claude-haiku-4-5") await ctx.session.run() ``` *** ## What Unpod Handles Everything audio-related is managed by Unpod on both phone and WebSocket sessions: | Capability | Phone | WebSocket | | -------------------- | ----- | --------- | | STT (transcription) | Yes | Yes | | VAD (turn detection) | Yes | Yes | | Barge-in detection | Yes | Yes | | TTS (synthesis) | Yes | Yes | | Recording | Yes | Yes | | Transcript storage | Yes | Yes | Your code only ever sees text. Media binding is handled entirely on Unpod's side. A media worker joins the LiveKit room for the session and bridges the caller's audio; your dialog brain connects over a separate text-only channel and never touches the audio stream or any SDP negotiation. This is why the same agent code runs unchanged across phone and WebSocket sessions. *** ## Next Steps Capacity, env vars, graceful shutdown, and the session controls - say(), transfer, end - that work for all session types. # API Reference Source: https://docs.unpod.ai/superdialog/api-reference Complete reference for SuperDialog - the unified DialogMachine entry point, the Playbook engine, sessions, tools, and adapters. SuperDialog ships **two engines, one entry point**. `DialogMachine` is the recommended way in; it drives either engine behind the same `Agent` protocol - the **Playbook engine by default**, the legacy graph runtime with `engine="flow"`. *** ## DialogMachine (unified entry point) ### Construction ```python Signature theme={null} DialogMachine( source: Flow | FlowSet | Playbook | str | dict, # path, parsed dict, or object llm: str | None = None, # model URI (Talker, and Director unless split) tools: list[Tool] | None = None, # any Tool; both engines memory: ContextStore | None = None, # graph-only; default in-memory config: dict | None = None, # graph-only: max_tokens, temperature, etc. traversal_dir: str | Path | None = None, # graph-only: auto-save traversal JSON adapter: str = "toolcall", # graph-only adapter selection *, engine: str = "auto", # "auto" | "playbook" | "flow" director_llm: str | None = None, # Playbook: strong-Director override ) ``` `source` is the artifact: a path string, a parsed dict, or an object. The unified loader auto-detects **full playbooks, the simple format, and legacy flow JSON** (compiled transparently), so you never route by format. **Engine selection** (`engine="auto"`, the default): a `Flow` / `FlowSet` object keeps the legacy graph engine (back-compat); a `Playbook` object, a path string, or a parsed dict runs the Playbook engine. Override with: * `engine="playbook"` - force the Playbook engine (compiling a flow if needed) * `engine="flow"` - force the legacy graph runtime (`ValueError` on a `Playbook`) In Playbook mode, `llm` is the Talker and the Director unless `director_llm` splits them; a missing `llm` raises a clear `ValueError`. Graph-only methods (`switch_flow`, `seed`, `load_flow_state`) raise `NotImplementedError` in Playbook mode, and `flow_state` returns `None`. ```python theme={null} from superdialog import DialogMachine agent = DialogMachine("booking.yaml", llm="openai/gpt-4.1-mini") # Playbook (default) agent = DialogMachine("booking.yaml", llm="anthropic/claude-haiku-4-5", director_llm="anthropic/claude-opus-4-7") # split Talker/Director dm = DialogMachine(Flow.load("kyc.json"), llm="...", engine="flow") # legacy graph ``` ### `turn` ```python Signature theme={null} async turn(text, context=None, stream=False) -> Turn | AsyncIterator[StreamChunk] ``` The primary method. Always async - drive it from `asyncio.run(...)` or any async runtime. ```python theme={null} reply = await agent.turn("hello") print(reply.text) stream = await agent.turn("hello", stream=True) # await the coroutine, then iterate async for chunk in stream: print(chunk.text, end="") ``` `Turn` carries `text`, `tool_calls`, and `metadata`. On the Playbook engine, `metadata` includes `checkpoint`, `version`, `ended`, and (on terminal checkpoints) `outcome`. **Streaming is real on the Playbook engine.** `PlaybookAgent` yields live provider tokens as the Talker produces them. The legacy graph engine resolves the turn in one shot and surfaces whitespace-delimited chunks (the `StreamChunk(text, done, turn)` shape is stable). ### `start` / `reset` / `set_llm` / `assist` ```python theme={null} opening = await agent.start() # agent greets first, no user input agent.reset() # clear memory, restart from the beginning agent.set_llm("anthropic/claude-haiku-4-5") # hot-swap; applies next turn agent.assist("Customer is upset. Be especially empathetic.") # system inject ``` ### `switch_flow` (graph engine only) ```python theme={null} dm.switch_flow("escalation") # FlowSet, engine="flow" dm.switch_flow("billing", preserve_memory=True) ``` Raises `NotImplementedError` on the Playbook engine - use multiple `journeys` and advance rules instead. *** ## Creating agents ### `generate_simple_playbook` (default) ```python Signature theme={null} async generate_simple_playbook(prompt, llm, *, max_attempts=3) -> str ``` Bootstrap a validated simple-format playbook from a description. The output is parsed and compiled before return, so a successful return is always loadable. CLI equivalent: `superdialog generate "" --output playbook.yaml`. ```python theme={null} from superdialog.playbook import generate_simple_playbook yaml_text = await generate_simple_playbook( "An agent that books demo calls and captures a day and time.", director, # any CompletesLLM ) open("playbook.yaml", "w").write(yaml_text) ``` ### `create_dialog_flow` (legacy) ```python Signature theme={null} async create_dialog_flow(prompt, llm, **kwargs) -> Flow ``` Bootstrap a legacy flow graph from a prompt. Prefer `generate_simple_playbook` for new agents; generated flows still run on the Playbook engine by default. The `llm` is used **only at construction**. *** ## Playbook engine Import from `superdialog.playbook`. Checkpoint-compound journeys: a fast **Talker** streams every spoken turn while an async **Director** extracts slots, judges advancement, and runs tools over an append-only event log. ### `Playbook` The authored artifact. Load it - all loaders auto-detect the simple format and legacy flow JSON: ```python theme={null} from superdialog.playbook import Playbook pb = Playbook.load(path) # YAML for .yaml/.yml, else JSON pb = Playbook.from_yaml(text) pb = Playbook.from_json(text) ``` Top-level fields: `persona`, `journeys` (≥1), `dispatch`, `tools`, `pipelines`, `handlers`, `interrupts`, `policies`, `middleware`, `env`, `views`, `initial`. Validation runs on construction and raises on unknown checkpoint/pipeline/tool refs, duplicate ids, undeclared `requires` keys, and the reserved `pipeline` result key. ### `PlaybookAgent` The engine behind the `Agent` protocol - drop it into `SessionWorker` and every host adapter unchanged. Use it directly when you want explicit Talker/Director LLMs or a custom HTTP executor. ```python theme={null} from superdialog.playbook import Playbook, PlaybookAgent, httpx_http agent = PlaybookAgent( playbook=Playbook.load("booking.yaml"), talker_llm=talker, # StreamsLLM director_llm=director, # CompletesLLM http=httpx_http, # HttpFn python_tools=None, # dict[str, PythonToolFn] | None token_budget=4000, # Talker view budget (estimated tokens) barrier_timeout=0.4, # hard gate: wait this long for the Director hold_timeout=None, # then filler + this much more before degrading ) ``` * **`async turn(text, *, stream=False)`** - Agent protocol. With `stream=True` the iterator yields **live provider tokens**, then any pass-through `say_verbatim` lines, then the `done=True` chunk. * **Barge-in safety** - aborting the stream interrupts *speech*, not the state machine: the Director runs to completion in a shielded scope. * **`assist`, `chat_ctx` / `load_chat_ctx`, `event_log` / `load_event_log`, `runtime`** - system inject, transcript view, the lossless log, and the `PlaybookRuntime`. ### The artifact model #### `Checkpoint` | Field | Meaning | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | Unique within its journey; referenced as `"journey.id"` | | `goal` | What "done" means; shown to Talker and Director | | `slots` | `dict[str, SlotSpec]` to extract while here | | `guidance` | Talker prose; Jinja over `{slots, views, results}` | | `say_verbatim` | Exact line; bypasses the Talker LLM | | `never_say` | Hard prohibitions injected into the Talker view | | `exit_say` | One-shot line spoken on the turn that *leaves* this checkpoint via a director rule (post-capture pitch); never on interrupt/policy advances. Simple format authors it as `then_say` | | `advance_when` | Ordered `AdvanceRule` list (outcome gates) | | `gate` | `soft` (default) or `hard` (barrier; `requires` need *confirmed* slots) | | `auto` | Speak verbatim once, then advance without user input | | `pipeline` | Pipeline run once on entry; routes on `pipeline.ok` / `pipeline.failed` | | `on_failure` | Route on pipeline failure or turn-budget exhaustion | | `terminal` + `outcome` | Entering ends the session with this outcome label | | `turn_budget` | User turns before a wrap-up steering note | #### `SlotSpec` `type` (`str int float bool date enum array object`), `required`, `values` (enum), `authoritative` (tool / pipeline / expr-`set` writes only), `invalidates` (slots/results cleared when this value changes), `description`. #### `AdvanceRule` `when` (prose for `judge: llm`, expr text for `judge: expr`), `judge` (`llm`/`expr`), `to` (target ref), `requires` (slots that must be filled at soft gates, confirmed at hard gates), `set` (confirmed slot writes on advance). Verdict-extracted slots are **provisional at hard gates** - a single verdict can never confirm its own `requires` in one shot. #### `ToolSpec` / `PipelineSpec` `ToolSpec`: `type` (`http`/`python`), `method`/`url`/`headers`/`body` (sandboxed Jinja over `{slots, env, results}`), `store_response_as`, `env_updates`, `run_once`, `when`, `timeout`, `args`. `PipelineSpec`: ordered `steps`, each with typed `on: {ok, failed, http_}` branches and a capped `RetrySpec` (`retry` ≤ 10). Tool failures are recorded as failed result events - data, never a crash. ### Protocols: `CompletesLLM`, `StreamsLLM`, `HttpFn`, `PythonToolFn` ```python theme={null} class CompletesLLM(Protocol): # Director seam async def complete(self, messages: list[dict[str, str]], **kwargs) -> str: ... class StreamsLLM(Protocol): # Talker seam def stream(self, messages: list[dict[str, str]], **kwargs) -> AsyncIterator[str]: ... HttpFn = Callable[..., Awaitable[tuple[int, Any]]] # (method=, url=, headers=, # body=, timeout=) -> (status, json) class PythonToolFn(Protocol): # registered via python_tools={id: fn} async def __call__(self, args: dict[str, Any], state: ConversationState) -> Any: ... ``` `httpx_http` is the production `HttpFn` backed by httpx. Any `superdialog.llm.LLMProvider` (the litellm-backed one behind model URIs) adapts to the Talker/Director protocols in a few lines - see [Embedding Guides](/superdialog/embedding-guides/overview). ### The expr language Used by `judge: expr` rules, `ToolSpec.when`, and `Playbook.views`. A safe, LLM-free, restricted Python expression over state: ```python theme={null} slots.city == "Pune" # slot value; None when unset results.hold.ok # tool result: ok / status / data / error results.search.status == 404 env.BOOKING_API # env lane (not available in views) pipeline.ok # pipeline-owned checkpoints only len(results.search.data.slots) > 0 first(pluck(results.search.data.slots, "time")) ``` Helpers (the only callables): `len`, `first`, `last`, `pluck`, `unique`, `min`, `max`, `any`, `all`. **Forbidden** (raises `ExprError`): arithmetic, comprehensions, lambdas, dict literals, f-strings, any `_`-prefixed name, non-whitelisted calls, expressions over 4096 chars. Missing values evaluate to `None` (falsy), never an exception. ### Migrating flows ```python theme={null} from superdialog import Flow from superdialog.playbook import compile_flow, coverage_report flow = Flow.load("golf_booking.json") pb = compile_flow(flow) # single-journey "main" Playbook report = coverage_report(flow, pb) # CoverageReport - the lossless proof assert not report.unmapped_nodes assert not report.unmapped_edges assert not report.unmapped_actions ``` `compile_flow` is lossless by construction: | Legacy construct | Becomes | | ---------------------------------- | -------------------------------------------------- | | Conversational nodes | Checkpoints in journey `"main"` | | Tool-free computational nodes | Folded into their sources' advance rules | | Tool-bearing computational chains | A `PipelineSpec` + synthetic checkpoint | | Hub routers (≥4-exit) | `dispatch` entries merged into inbound checkpoints | | Silence nodes | `policies.silence` | | Token-expiry global edge + refresh | `middleware` | | Other global edges | `interrupts` | | Webhook/timer system nodes | `handlers` | | `global_actions` | `tools`, 1:1 | Deterministic edge conditions (`X.success == true`, `X.status == 404`) compile to `judge: expr`; everything else stays `judge: llm` with the prose verbatim. `coverage_report` lists anything that didn't map (any `unmapped_*` entry is a compiler bug) - run it in CI. ### `EventLog` and `ConversationState` The event log is the single source of truth; state is a pure fold over it. ```python theme={null} from superdialog.playbook import ConversationState, EventLog text = agent.event_log.to_jsonl() # persist (JSONL, one event/line) agent.load_event_log(EventLog.from_jsonl(text)) # lossless restore state = ConversationState.fold(agent.event_log, playbook) ``` `EventLog` is append-only with contiguous versions from 1. Events are frozen, discriminated on `type`: `utterance`, `slot_write`, `advance`, `steering_note`, `tool_call`, `tool_result`, `env_write`, `scratchpad`, `summary`, `external`, `degraded`, `session_end`. `ConversationState.fold` derives `checkpoint_id`, `slots` (value + provisional/confirmed status), `transcript`, `env`, `tool_results`, `ended`, `outcome`, and helpers `slot_value`, `confirmed`, `filled`. ### `replay` and the eval bridge ```python theme={null} from superdialog.playbook import replay, run_session, run_eval, PersonaSpec report = await replay(log, playbook, director_llm) # pure: never mutates log report.stable # every decision matched? metrics = await run_session(agent, persona, user_llm) report = await run_eval(playbook_factory=make_agent, personas=personas, user_llm=user_llm, n=1) report.completion_rate, report.mean_slot_accuracy ``` `replay` re-runs the Director over recorded utterances under a (possibly edited) playbook and diffs decisions - regression evidence for prompt or model changes. The eval bridge scores persona self-play from the same logs. *** ## Sessions (v0.2) Sessions add lifecycle and persistence on top of any `Agent`-protocol brain - `PlaybookAgent` (default) and the legacy `DialogMachine` alike. ### Agent Protocol ```python Signature theme={null} class Agent(Protocol): async def turn(text: str, *, stream: bool = False) -> TurnResult | AsyncIterator[StreamChunk] def assist(text: str) -> None @property def chat_ctx(self) -> ChatContext def load_chat_ctx(ctx: ChatContext) -> None ``` ### `SessionWorker` / `SessionHandle` ```python theme={null} from superdialog import DialogMachine, SessionWorker, InMemorySessionStore worker = SessionWorker( agent_factory=lambda: DialogMachine("booking.yaml", llm="openai/gpt-4.1-mini"), store=InMemorySessionStore(), lock_backend=None, # default: AsyncioLockBackend max_sessions=1000, ) async with worker.acquire("user-42") as h: result = await h.turn("hello") h.assist("Customer sounds upset; be empathetic.") ``` `agent_factory` is called once per new session; `acquire(session_id)` loads or creates the session, locks it for the block, and persists on exit. Different ids run in parallel; same id serialises. `SessionHandle` exposes `turn`, `assist`, `state`. ### Session stores and lock backends | Store | Ships | Use case | | ------------------------------------------------------------- | ---------- | ------------------------------------------------ | | `InMemorySessionStore` | ✅ v0.2 | Single-process; state lives for process lifetime | | `NullSessionStore` | ✅ v0.2 | Voice calls; no persistence wanted | | `RedisSessionStore`, `FileSessionStore`, `SQLiteSessionStore` | 🔜 planned | Distributed / durable | | Backend | Ships | Use case | | -------------------- | ---------- | --------------------------- | | `AsyncioLockBackend` | ✅ v0.2 | Single-process | | `RedisLockBackend` | 🔜 planned | Multi-process / distributed | `SessionWorker`'s `SessionRecord` persists `chat_ctx` / `flow_state` only. For durable Playbook-engine resume, persist `agent.event_log.to_jsonl()` and restore with `load_event_log` - see [Sessions](/superdialog/sessions). ### Other agent brains ```python theme={null} from superdialog import LLMAgent, LangChainAgent, SessionWorker, InMemorySessionStore worker = SessionWorker( agent_factory=lambda: LLMAgent(llm="openai/gpt-4.1-mini", system_prompt="Be helpful."), store=InMemorySessionStore(), ) # LangChainAgent(runnable=...) requires: pip install superdialog[langchain] ``` *** ## Tools `PythonTool`, `HttpTool`, and `MCPTool` implement the `Tool` ABC and are passed through `DialogMachine(tools=[...])` on either engine. On the Playbook engine, prefer declaring tools in the playbook's process layer (see [Tools](/superdialog/tools)). ```python theme={null} from superdialog import PythonTool, HttpTool, MCPTool, Tool import os PythonTool.of(lookup_customer) # infer id/name/schema PythonTool(id="lookup", name="lookup", description="...", fn=lookup_customer) HttpTool(id="lookup", name="lookup", description="Look up a customer", url="https://api.company.io/lookup", method="POST", auth={"type": "bearer", "token": os.environ["KEY"]}) MCPTool(id="search", name="search", description="Search the KB", server="https://mcp.company.io") Tool.from_dict({"type": "http", "id": "lookup", "name": "lookup", "description": "...", "url": "https://api.company.io/lookup"}) ``` HTTP `auth` accepts `{"type": "bearer", "token": "..."}` in v0.2 (`basic`, `api_key`, callable planned). *** ## LLM provider registration ```python theme={null} import os from superdialog import DialogMachine, register_llm_provider register_llm_provider( name="internal", base_url="https://llm.company.io/v1", api_key=os.environ["INTERNAL_KEY"], api_style="openai", ) agent = DialogMachine("booking.yaml", llm="custom/internal/llama-3-70b-tuned") ``` Process-global. Once registered, `custom//` works in `DialogMachine(llm=...)`, `set_llm()`, and `create_dialog_flow(llm=...)`. *** ## Adapters | Import | Purpose | | ------------------------------------------------ | --------------------------------------------------- | | `superdialog.adapters.livekit.DialogMachineLLM` | LiveKit `Agent(llm=...)` plugin (accepts any Agent) | | `superdialog.adapters.pipecat.make_processor` | Factory for PipeCat `FrameProcessor` | | `superdialog.adapters.fastapi.FastAPIRouter` | Mountable router: `/turn`, `/stream`, `/reset` | | `superdialog.adapters.websocket.WebSocketRunner` | Standalone WSS server for Unpod Voice Infra | See [Embedding Guides](/superdialog/embedding-guides/overview) for complete integration examples per host. # Architecture Source: https://docs.unpod.ai/superdialog/architecture How SuperDialog works internally - two engines behind one Agent protocol, the Talker/Director runtime, the event-sourced log, tools, sessions, and adapters. ## Two engines, one contract One Python package. No services, no daemons. Everything in-process. SuperDialog ships **two conversation engines** behind the same `Agent` protocol (`turn` / `assist` / `chat_ctx` / `load_chat_ctx`). Hosts, sessions, and adapters do not know which engine they are driving. Animated SuperDialog engine contract diagram showing host platforms flowing through adapters, SessionWorker, the Agent protocol, and then branching to PlaybookAgent and DialogMachine engines. * **Engine B - Playbook (default).** Checkpoint-compound runtime: a Talker and a Director over an event-sourced log. Internals below. * **Engine A - DialogMachine (legacy).** Graph-railed state machine, fully supported; flow JSON runs compiled onto Engine B by default. `DialogMachine(source, llm, *, engine=...)` is the recommended way in and drives either engine - the Playbook engine by default, the legacy graph runtime with `engine="flow"`. What each engine is for: [What is SuperDialog?](/superdialog/introduction). ## Library shape ``` superdialog/ ├─ flow/ # Flow graph: nodes, edges, serialization ├─ machine/ # DialogStateMachine engine (Engine A internals) ├─ dialog_machine.py # Public DialogMachine facade (unified entry point) ├─ playbook/ # Playbook engine (Engine B): models, events, │ # runtime, talker, director, compiler, replay ├─ agent.py # Agent Protocol + TurnResult ├─ agents/ # LLMAgent, LangChainAgent (non-DM brains) ├─ session/ # Session, SessionHandle, SessionWorker, stores, locks ├─ chat_context.py # ChatContext, ChatMessage (LiveKit-aligned) ├─ llm/ # Model URI resolver and provider adapters ├─ tools/ # Python / HTTP / MCP tool wrappers ├─ cli/ # superdialog generate / chat / optimize / playbook / flow / eval └─ adapters/ # LiveKit, PipeCat, FastAPI, WebSocket ``` ## Engine B - the Playbook runtime The default engine runs declarative **checkpoint** journeys. Two LLM roles share one append-only event log: * A fast **Talker** streams every spoken turn with one LLM call. * An async **Director** makes one structured call per user utterance to extract typed slots, judge advance rules, run tools, and write a steering note. ### One turn, in order Animated Playbook turn runtime diagram showing user text entering PlaybookAgent.turn, splitting into a shielded Director task and Talker stream, joining, and returning checkpoint and outcome data. 1. **User text arrives.** The agent snapshots state (version *N*) for the Talker. 2. **Director starts concurrently** in a cancellation-shielded task: appends the utterance, then makes **one structured call** that extracts slots, judges the advance rules, and writes a 1-3 sentence steering note. 3. **Talker streams concurrently** from snapshot *N* - persona, guidance, steering note, slots, and recent transcript packed into one streaming call; tokens go straight to the host. At a hard gate it barriers first. 4. **Quiescence.** After the verdict is applied, the runtime hops until nothing moves: the entered checkpoint's pipeline runs, `judge: expr` rules evaluate LLM-free, `auto` checkpoints speak and advance, and a terminal checkpoint ends the session with its outcome. 5. **Join and repair.** The Talker's speech is logged once; `check_repairs` compares it against later slot writes and nudges a self-correction if the Talker re-asked something already answered. Barge-in is safe by construction: aborting the stream cancels *speech*, not the state machine - the Director runs to completion in a shielded scope. ### The event-sourced log Every mutation is an event; state is a pure fold over the log; the log is the audit artifact. ```python theme={null} from superdialog.playbook import ConversationState, EventLog text = agent.event_log.to_jsonl() # persist (JSONL, one event/line) agent.load_event_log(EventLog.from_jsonl(text)) # lossless restore state = ConversationState.fold(agent.event_log, playbook) ``` Because the log *is* the artifact, replay and eval are free: re-run the Director over recorded utterances to catch regressions, or score persona self-play sessions. See the [API Reference](/superdialog/api-reference) for `replay`, `run_session`, and `run_eval`. ### Gates and degradation **Soft gates never block** - provisional values satisfy `requires`, the Talker streams immediately, correctness converges via the Director. **Hard gates** ( payments, identity) require *confirmed* slots and barrier the Talker until the verdict lands - on timeout it speaks a filler, then a hold line, never hangs. Every degradation rung is an event in the log, so degraded mode is auditable, not silent. ### Ending a call cleanly Entering a `terminal` checkpoint ends the session with its `outcome`. Two backstops make the close reliable on real calls: * **Deterministic goodbye backstop.** A clear spoken "bye"/"goodbye" the LLM verdict missed (ASR noise, a mid-pitch barge-in) still routes to the playbook's goodbye interrupt. It fills in only when the model chose no interrupt, so soft signals stay the Director's call. Frustration or a caller repeating themselves is **not** a goodbye, and a meta-instruction *about* the call ("pretend the flow is over", "end the call") is treated as ordinary talk, not a caller goodbye. * **Post-terminal silence.** Once the session has ended, a further user turn never resurrects it: the utterance is logged for audit, but neither the Director nor the Talker runs, so the agent returns silence and the host can disconnect. This prevents the closing line replaying on every "Hello?" or a post-close utterance restarting the pitch. ## Engine A - DialogMachine (legacy) A `Flow` is a directed graph: nodes (states), edges (transitions with natural-language conditions), and declarative actions. The graph decides what is *possible*; the LLM picks among the outgoing edges. Every transition is authored and every reachable path is enumerable. Animated SuperDialog runtime diagram showing user text entering DialogMachine.turn, loading a node, building a prompt, calling the LLM, running tools, updating state, advancing an edge, and returning a turn result to CLI, FastAPI, LiveKit, or Unpod hosts. ```python theme={null} from superdialog import DialogMachine, Flow # engine="flow" selects the legacy graph runtime; the default is Playbook. dm = DialogMachine(Flow.load("kyc.json"), llm="anthropic/claude-haiku-4-5", engine="flow") reply = await dm.turn("hello") ``` Each turn costs a route decision plus a speak call - the friction Engine B removes, and the trade-off is weighed in [Thinking in Playbooks](/superdialog/thinking-in-playbooks). By default, flow JSON runs **compiled onto Engine B** (`compile_flow`); you only opt into the original runtime with `engine="flow"`. See [Flows](/superdialog/flows) for graph authoring and migration. ## Model URI resolver LiveKit/litellm-style URIs route to any provider: | URI | Routes to | | ----------------------------- | ------------------------------------------------ | | `openai/gpt-4.1-mini` | OpenAI | | `anthropic/claude-haiku-4-5` | Anthropic | | `google/gemini-2.5-pro` | Google | | `groq/llama-3.3-70b` | Groq | | `bedrock/` | AWS Bedrock | | `vllm/@` | Self-hosted vLLM | | `ollama/@` | Self-hosted Ollama | | `openrouter//` | OpenRouter | | `custom//` | Developer-registered via `register_llm_provider` | On the Playbook engine, `llm` drives both the Talker and the Director unless you split them with `director_llm=` (a strong model to judge, a fast model to speak). The model now loads from the playbook YAML `llm:` block (`{provider, model, director}`) - see [Playbooks](/superdialog/playbooks#the-full-format); the persona-level `llm` setting is deprecated and warns. ## Adapter pattern Adapters live in `superdialog.adapters` and are thin shims. The same agent - `PlaybookAgent` or legacy `DialogMachine` - passes through all of them. | Adapter | Use case | | ---------------------------- | -------------------------------------------------- | | `DialogMachineLLM` (LiveKit) | Plug into `Agent(llm=...)` (accepts any Agent) | | `make_processor` (PipeCat) | Factory for `FrameProcessor` in a pipeline | | `FastAPIRouter` | Mountable router with `/turn`, `/stream`, `/reset` | | `WebSocketRunner` | Standalone WSS server for Unpod Voice Infra | ## What lives outside this library SuperDialog ends at text in, text out - on both engines. The following are out of scope: * Audio processing * STT, TTS * Telephony, SIP, RTP * Media servers and WebRTC Rooms * Phone numbers, voice profiles * Billing # CLI Reference Source: https://docs.unpod.ai/superdialog/cli All superdialog command-line commands - the playbook-default workflow first, the legacy flow-graph commands second. ## Install The CLI is included when you install SuperDialog: ```bash theme={null} pip install superdialog superdialog --help ``` The default commands operate on **playbooks** and run on the Playbook engine. The `flow` sub-tree (and `--mode flow`) is the legacy graph path. *** ## `superdialog generate` The default creation path. Bootstrap a validated simple-format **playbook** from a plain-language prompt. ```bash theme={null} superdialog generate "Confirm KYC. Ask for Aadhaar last 4. Confirm DOB." \ --output kyc.yaml ``` | Flag | Default | Description | | ---------- | --------------------- | --------------------------------------------- | | `--output` | `playbook.yaml` | Output file path | | `--llm` | `openai/gpt-4.1-mini` | Model URI used to generate | | `--from` | - | Read the prompt from a file instead of inline | The output is parsed and compiled before it's written, so anything `generate` produces is loadable. **When to use:** start every new agent here, then refine the YAML by hand. *** ## `superdialog chat` Interactive terminal chat. No infrastructure, no Unpod account, no phone number. Runs on the **Playbook engine**; defaults to `./playbook.yaml`, then `./flow.json` - any format is auto-detected. ```bash theme={null} superdialog chat kyc.yaml ``` ``` > Hello, I need to verify my KYC. Agent: Sure! Could you please provide the last 4 digits of your Aadhaar? > 1234 Agent: Thank you. Could you also confirm your date of birth? [checkpoint=collect_dob ended=False] ``` The per-turn status line names the live checkpoint, so you can watch outcomes advance. | Flag | Default | Description | | ----------------- | ----------------------------- | ----------------------------------------------------------------------------------- | | `--flow` | `playbook.yaml` → `flow.json` | Path to the artifact (any format) | | `--llm` | `openai/gpt-4.1-mini` | Model URI for the runtime | | `--mode` | `playbook` | `playbook` (default) or `flow` (legacy graph engine) | | `--adapter` | `toolcall` | Applies **only in `--mode flow`**: `toolcall` (1 call/turn) or `llm` (2 calls/turn) | | `--traversal-dir` | - | Save session JSON on completion (graph engine) | **When to use:** during playbook (or legacy flow) design, prompt tuning, and eval-dataset collection - before any voice infrastructure is involved. The CLI reads `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` from your environment. ### The build loop `superdialog generate` a playbook from a plain-language prompt. Full end-to-end dialog in the terminal - same logic, same LLM calls, same tool execution, nothing but Python. Refine the prose, slots, and advance rules. Repeat. Only once it behaves correctly, wire it into [LiveKit](/superdialog/embedding-guides/livekit), [PipeCat](/superdialog/embedding-guides/pipecat), a [FastAPI endpoint](/superdialog/embedding-guides/fastapi), or [Unpod Voice Infra](/superdialog/embedding-guides/unpod-voice). ### Which engine am I on? The status line tells you: | Status line | Engine | | -------------------------------- | ------------------------------------ | | `[checkpoint= ended=]` | Playbook engine (the default) | | `[ms]` | Legacy DialogMachine (`--mode flow`) | A bare `--flow x.json` shows the checkpoint form - flow JSON is compiled onto the Playbook engine. ### REPL loop in Python For more control - custom tools, a split Talker/Director, or to inspect the event log: ```python theme={null} import asyncio from superdialog import DialogMachine agent = DialogMachine("kyc.yaml", llm="anthropic/claude-haiku-4-5") # any format async def main(): while True: user = input("> ") if user.strip() in ("quit", "exit"): break reply = await agent.turn(user) print(reply.text) asyncio.run(main()) ``` **Inspect the event log.** Drop to `PlaybookAgent` and `agent.event_log.to_jsonl()` is the audit artifact - every utterance, slot write, advance, and tool call, replayable offline: ```python theme={null} from superdialog.playbook import Playbook, PlaybookAgent, httpx_http agent = PlaybookAgent( playbook=Playbook.load("kyc.yaml"), talker_llm=talker, director_llm=director, http=httpx_http, ) ``` **Legacy graph engine in code.** Construct `DialogMachine(Flow.load("kyc.json"), llm=..., engine="flow", traversal_dir="./traversal_history")` and drive the same loop - see [Traversal history](#traversal-history-graph-engine). *** ## `superdialog optimize` Reflective prose optimizer: paired persona evals score targeted, prose-only edits and emit improved YAML **in your source format**. ```bash theme={null} superdialog optimize --playbook kyc.yaml ``` It generates persona suites, runs paired evals (before/after), makes prose-only edits to `guidance` / `say`, and writes back improved YAML. **When to use:** to close the run → eval → improve loop without hand-tuning prompts. *** ## `superdialog playbook` Migration and direct playbook operations. ```bash theme={null} superdialog playbook compile kyc.json # compile legacy flow JSON → playbook YAML superdialog playbook chat --playbook kyc.yaml # REPL against an existing playbook superdialog playbook run kyc.json # compile a flow and immediately chat ``` **When to use:** migrating an existing flow graph to a playbook, or running a playbook explicitly. *** ## `superdialog eval` A subcommand group: the **playbook-vs-vanilla A/B harness** plus the legacy single-session audit. Full guide: [A/B Evals](/superdialog/evals). ```bash theme={null} superdialog eval gen-dataset --playbook spa.yaml --n-probes 8 # build the dataset superdialog eval run --playbook spa.yaml --dataset spa.evalcases.yaml --out ./eval-out superdialog eval bench --playbook spa.yaml --models openai/gpt-4o-mini --max-turns 20 # one shot superdialog eval serve --playbook spa.yaml --port 8000 # OpenAI-compatible endpoint superdialog eval suite --config suites.yaml --tier smoke # CI-able behavioral gate superdialog eval flow --flow kyc.json --traversal session.json # legacy session audit ``` | Subcommand | Purpose | | ------------- | ------------------------------------------------------------------------------------------------------------- | | `gen-dataset` | Build `.evalcases.yaml` (personas + probes) offline | | `run` | A/B both modes over a dataset, write `report.json` + `report.md` | | `bench` | One shot: gen dataset (if missing) + A/B every `--models` entry, one report dir each | | `serve` | Serve the playbook as an OpenAI-compatible endpoint for any benchmark | | `suite` | Run a suite registry as a CI gate; assert behavioral expectations (`--tier smoke\|full`, `--only`, `--force`) | | `flow` | Legacy: audit a recorded session (`--traversal`) or run a synthetic eval | `eval run` takes `--modes`, `--agent-model`, `--director-model`, `--talker-model`, `--judge-model`, `--user-model`, `--metrics`, and `--repeats` - see the [A/B Evals](/superdialog/evals) guide. (For playbook persona evals from Python, see `run_eval` in the [API Reference](/superdialog/api-reference#replay-and-the-eval-bridge).) *** ## `superdialog benchmark` A separate RAGAS + deterministic harness: replays a dataset's user turns at one or more models and scores **raw LLM vs with-SuperDialog** against ground truth in one big table. ```bash theme={null} superdialog benchmark --data universal --flow kyc.yaml --prompt raw_system.txt ``` | Flag | Default | Description | | -------------------------- | --------------------------------------- | ---------------------------------------------------------- | | `--data` | `universal` | Dataset short name or path to a `.jsonl` | | `--flow` | dataset's `playbook` | Playbook YAML to run | | `--prompt` | - | Raw-LLM system-prompt `.txt` (needed for the raw baseline) | | `--models` | `gpt-4o-mini,gpt-4.1-mini,claude-haiku` | Models to score | | `--sd-only` / `--raw-only` | - | Restrict to one side | | `--no-ragas` | - | Deterministic metrics only (no judge; fast/free) | | `--out` | - | Write the report table to this path | `benchmark` uses the RAGAS 0.2.x line (the `benchmark` extra), while the A/B `eval` harness uses RAGAS 0.4.3 (the `ragas` extra). They cannot co-install - see [A/B Evals → RAGAS](/superdialog/evals#ragas-is-optional-and-version-pinned). *** ## Legacy: flow graphs The `flow` sub-tree authors and inspects **flow graphs**. These still work; `superdialog generate` writes a playbook instead. ```bash theme={null} # Validate graph structure (unreachable nodes, missing edges, undefined slots) superdialog flow lint kyc.json # Render a Mermaid diagram of the graph superdialog flow draw kyc.json # Bootstrap a flow.json from a prompt (legacy; equivalent to create_dialog_flow) superdialog flow generate "Confirm KYC. Ask for Aadhaar last 4." \ --llm openai/gpt-5.1 --output kyc.json # Run the original graph runtime in the REPL superdialog chat kyc.json --mode flow ``` By default a flow JSON runs **compiled onto the Playbook engine** - `--mode flow` opts into the original graph runtime. See [Flows (legacy)](/superdialog/flows). *** ## Traversal history (graph engine) Any command running the legacy graph engine supports `--traversal-dir`. When set, a timestamped JSON file is written per completed session capturing every node visited, every turn, and all collected slot values: ```bash theme={null} superdialog chat kyc.json --mode flow --traversal-dir ./traversal_history ``` Use these files to build eval corpora, debug flow paths, and audit conversations. On the Playbook engine, the equivalent artifact is the event log (`agent.event_log.to_jsonl()`). # FastAPI Source: https://docs.unpod.ai/superdialog/embedding-guides/fastapi Expose a SuperDialog dialog machine as a REST endpoint for text chatbots, web widgets, and async messaging. ## When to use this * Text-only chatbot (no voice) * Support widget (Intercom, Zendesk, custom) * WhatsApp or SMS webhook * Any HTTP-based channel ## Single-user (stateless) For simple cases where one machine handles one conversation at a time: ```python theme={null} from fastapi import FastAPI from superdialog import DialogMachine app = FastAPI() agent = DialogMachine("kyc.yaml", llm="openai/gpt-4.1-mini") # any format @app.post("/turn") async def turn(payload: dict): reply = await agent.turn(payload["text"]) return {"reply": reply.text} ``` ## Multi-user (SessionWorker) For multi-user or multi-worker deployments, route each conversation through a `SessionWorker`: ```python theme={null} from contextlib import asynccontextmanager from fastapi import FastAPI from superdialog import DialogMachine, SessionWorker, InMemorySessionStore worker: SessionWorker @asynccontextmanager async def lifespan(app: FastAPI): global worker worker = SessionWorker( agent_factory=lambda: DialogMachine("booking.yaml", llm="openai/gpt-4.1-mini"), store=InMemorySessionStore(), # swap for a distributed SessionStore in production ) yield app = FastAPI(lifespan=lifespan) @app.post("/turn") async def turn(payload: dict): async with worker.acquire(payload["session_id"]) as h: result = await h.turn(payload["text"]) return {"reply": result.text} ``` The `SessionWorker`: * Creates one agent per active session * Shares the immutable playbook by reference * Serialises concurrent requests for the same `session_id` * Runs concurrent requests for different session IDs fully in parallel `result.metadata` carries `checkpoint`, `version`, `ended`, and (on terminal checkpoints) `outcome`. ## Streaming endpoint On the Playbook engine the stream is **live provider tokens** from the Talker - not post-hoc chunking: ```python theme={null} from fastapi import FastAPI from fastapi.responses import StreamingResponse from superdialog import DialogMachine app = FastAPI() agent = DialogMachine("kyc.yaml", llm="anthropic/claude-haiku-4-5") @app.post("/stream") async def stream(payload: dict): async def generate(): stream = await agent.turn(payload["text"], stream=True) async for chunk in stream: yield chunk.text return StreamingResponse(generate(), media_type="text/plain") ``` ## Using FastAPIRouter The built-in adapter mounts `/turn`, `/stream`, and `/reset` in one line: ```python theme={null} from fastapi import FastAPI from superdialog import DialogMachine from superdialog.adapters.fastapi import FastAPIRouter agent = DialogMachine("kyc.yaml", llm="openai/gpt-4.1-mini") app = FastAPI() app.include_router(FastAPIRouter(agent), prefix="/dialog") # Exposes: POST /dialog/turn, POST /dialog/stream, POST /dialog/reset ``` ## Request / response shape ```json theme={null} // POST /turn { "text": "Hello, I need to verify my KYC.", "session_id": "user-42" } // Response { "reply": "Sure! Could you please provide the last 4 digits of your Aadhaar?" } ``` ## Deploying to production For production multi-worker FastAPI: 1. Replace `InMemorySessionStore` with a distributed `SessionStore` (`RedisSessionStore` is planned; implement the `SessionStore` protocol today) so state survives across workers 2. Set `max_sessions` on `SessionWorker` to cap memory usage 3. Use `NullSessionStore` if your sessions are fully stateless (e.g. webhook-per-message pattern) The in-process `SessionWorker` works as-is because agents stay cache-resident, but durable or multi-worker resume on the Playbook engine requires persisting `agent.event_log.to_jsonl()` yourself and restoring via `load_event_log` - `SessionWorker`'s `SessionRecord` persists `chat_ctx` / `flow_state` only, which loses playbook state fidelity. External events (webhooks, timers, silence) go to `agent.runtime.on_external(...)` from your own endpoints. # LiveKit Source: https://docs.unpod.ai/superdialog/embedding-guides/livekit Plug SuperDialog into a LiveKit agent as the LLM brain. ## How it works SuperDialog ships a `DialogMachineLLM` plugin (named for the legacy engine, but it accepts **any** superdialog `Agent`) that wires an agent into a LiveKit `Agent` via the `llm=` parameter - the same pattern LiveKit's own `livekit-plugins-langchain` uses. LiveKit's `AgentSession` drives the conversation (STT → LLM → TTS). `DialogMachineLLM` sits in the LLM slot and translates between LiveKit's `ChatContext` and SuperDialog's `turn()` API. On the Playbook engine (the default), **streaming is real**: the Talker's tokens reach TTS as they are generated, and a barge-in (the host aborting the stream mid-utterance) interrupts speech, never the state machine - the Director's decision still lands. ## Install ```bash theme={null} pip install superdialog livekit-agents ``` ## Minimal example ```python theme={null} from livekit.agents import Agent, AgentSession, JobContext, WorkerOptions, cli from superdialog import DialogMachine from superdialog.adapters.livekit import DialogMachineLLM dm = DialogMachine("kyc.yaml", llm="anthropic/claude-haiku-4-5") # any format async def entrypoint(ctx: JobContext): agent = Agent(llm=DialogMachineLLM(dm)) await AgentSession().start(agent=agent, room=ctx.room) if __name__ == "__main__": cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint)) ``` ## With STT and TTS ```python theme={null} from livekit.agents import Agent, AgentSession, JobContext, WorkerOptions, cli from livekit.plugins import deepgram, cartesia from superdialog import DialogMachine from superdialog.adapters.livekit import DialogMachineLLM dm = DialogMachine("kyc.yaml", llm="anthropic/claude-haiku-4-5") async def entrypoint(ctx: JobContext): await ctx.connect() agent = Agent( llm=DialogMachineLLM(dm), stt=deepgram.STT(), tts=cartesia.TTS(), ) await AgentSession().start(agent=agent, room=ctx.room) if __name__ == "__main__": cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint)) ``` ## Per-call dialog machine For production, create a fresh agent per call so conversation state is isolated: ```python theme={null} from superdialog import DialogMachine, PythonTool async def entrypoint(ctx: JobContext): await ctx.connect() # Fresh agent per call dm = DialogMachine( "kyc.yaml", llm="anthropic/claude-haiku-4-5", tools=[PythonTool.of(lookup_customer)], ) agent = Agent(llm=DialogMachineLLM(dm)) await AgentSession().start(agent=agent, room=ctx.room) ``` **Advanced / legacy.** Pass a `PlaybookAgent` for explicit Talker/Director LLMs, or `DialogMachine(Flow.load("kyc.json"), llm="anthropic/claude-opus-4-7", engine="flow")` for the legacy graph engine - same adapter, same wiring. Voice-event plumbing (feeding silence timeouts into `agent.runtime.on_external`) is roadmap; today the adapter covers the text path. ## Mid-call context injection Push system instructions during a call with `assist`: ```python theme={null} # After detecting customer sentiment, inject context dm.assist("The customer sounds frustrated. Prioritise empathy and resolution speed.") ``` ## When to use this adapter * You're already using LiveKit for media routing (rooms, WebRTC, recording) * You want SuperDialog to manage turn-by-turn dialog logic * You need a clean separation between media transport (LiveKit) and conversation logic (SuperDialog) # Embedding Overview Source: https://docs.unpod.ai/superdialog/embedding-guides/overview How to plug SuperDialog into any host environment - CLI, LiveKit, PipeCat, FastAPI, Unpod Voice, and more. ## The same pattern everywhere In every host, three things stay the same: 1. **Construct an entry point** - `DialogMachine(source, llm=...)`. It runs the Playbook engine by default; `source` accepts full playbooks, simple-format playbooks, *and* legacy flow JSON (auto-compiled), so you don't pick a format, you just point it at your artifact. 2. **Route inbound text** to `agent.turn(text)`. 3. **Send the reply text** back to the host's output channel. Both engines implement the same `superdialog.agent.Agent` protocol (`turn` / `assist` / `chat_ctx` / `load_chat_ctx`), so every adapter accepts either one. The host varies; the SuperDialog code is identical. ```python theme={null} # This object works in every host below from superdialog import DialogMachine agent = DialogMachine("booking.yaml", llm="anthropic/claude-haiku-4-5") # any format ``` **Advanced:** drop to `PlaybookAgent` when you need to supply the two LLM seams directly - a `StreamsLLM` Talker and a `CompletesLLM` Director - or a custom HTTP executor. Any `superdialog.llm.LLMProvider` (the litellm-backed one behind model URIs) adapts in a few lines: ```python theme={null} from superdialog.llm import resolve_llm class TextLLM: def __init__(self, provider): self._p = provider async def complete(self, messages, **kw): return (await self._p.complete(messages, **kw)).text async def stream(self, messages, **kw): async for chunk in self._p.stream(messages, **kw): if chunk.text: yield chunk.text talker = TextLLM(resolve_llm("anthropic/claude-haiku-4-5")) # fast: speaks director = TextLLM(resolve_llm("anthropic/claude-opus-4-7")) # strong: judges ``` Why two models, and how the Director steers the Talker without stalling it: [Architecture](/superdialog/architecture). ## Choose your host Zero infrastructure. Best for testing and prompt tuning. Voice agent via `Agent(llm=DialogMachineLLM(...))` plugin. Drop-in `FrameProcessor` for PipeCat pipelines. REST endpoint for text chatbots and web widgets. Plug your `DialogMachine` into an Unpod `AgentRunner` session - no extra server needed. Because SuperDialog is text-only, every dialog is unit-testable. ## Lines of code comparison | Host | Adapter | Extra LoC | | ----------------------------- | ------------------------------------------------------- | --------- | | CLI | None - direct `input()`/`print()` or `superdialog chat` | \~5 | | LiveKit | `DialogMachineLLM` | \~8 | | PipeCat | `make_processor` | \~12 | | FastAPI | `FastAPIRouter` or direct route | \~6 | | Unpod Voice (SDK) | `unpod.AgentRunner` + `session.dialog_machine` | \~6 | | Unit test | None - direct calls | \~3 | | Custom (Slack, Discord, etc.) | None - direct callback | \~3 | # PipeCat Source: https://docs.unpod.ai/superdialog/embedding-guides/pipecat Use SuperDialog as the LLM processor in a PipeCat voice pipeline. ## How it works SuperDialog ships a `make_processor` factory that builds a PipeCat `FrameProcessor` wrapping **any** superdialog `Agent`. Because PipeCat's `FrameProcessor` base class shifts between releases, SuperDialog synthesises the right subclass against whichever PipeCat version is installed. ## Install ```bash theme={null} pip install superdialog pipecat-ai ``` ## Minimal example ```python theme={null} from superdialog import DialogMachine from superdialog.adapters.pipecat import make_processor agent = DialogMachine("kyc.yaml", llm="anthropic/claude-haiku-4-5") # any format processor = make_processor(agent) ``` **Legacy / advanced:** `make_processor(DialogMachine(Flow.load("kyc.json"), llm=..., engine="flow"))` or a hand-built `PlaybookAgent` - same factory, same pipeline position. ## Full pipeline ```python theme={null} from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineTask from pipecat.services.deepgram import DeepgramSTTService from pipecat.services.cartesia import CartesiaTTSService from superdialog import DialogMachine from superdialog.adapters.pipecat import make_processor async def main(): agent = DialogMachine("kyc.yaml", llm="anthropic/claude-haiku-4-5") pipeline = Pipeline([ DeepgramSTTService(api_key="..."), # STT make_processor(agent), # SuperDialog as the LLM CartesiaTTSService(api_key="..."), # TTS ]) runner = PipelineRunner() task = PipelineTask(pipeline) await runner.run(task) ``` ## Per-call processor For production, create a fresh agent and processor per call: ```python theme={null} async def handle_call(): agent = DialogMachine("kyc.yaml", llm="anthropic/claude-haiku-4-5") processor = make_processor(agent) pipeline = Pipeline([stt, processor, tts]) await PipelineRunner().run(PipelineTask(pipeline)) ``` ## When to use this adapter * You have an existing PipeCat-based voice stack * You want SuperDialog to replace hand-written LLM logic between STT and TTS * Your STT and TTS are already configured in PipeCat # Testing Source: https://docs.unpod.ai/superdialog/embedding-guides/testing Test your conversations as pure functions - no audio, no infrastructure. Scripted LLMs for offline tests, replay and persona evals for regression. ## Why SuperDialog is easy to test SuperDialog is text in, text out. There is no audio to mock, no WebRTC room to spin up, no telephony to stub. Every dialog is a Python function that takes a string and returns a string. This is the killer feature vs. voice-coupled frameworks where tests need audio fixtures. ## Setup ```bash theme={null} pip install pytest pytest-asyncio ``` ```toml theme={null} [tool.pytest.ini_options] asyncio_mode = "auto" ``` Or use `anyio` as specified in the project guidelines. ## Offline tests with scripted LLMs The Playbook engine separates the **Talker** (`StreamsLLM`) and **Director** (`CompletesLLM`) seams, so you can run a conversation with **no network** by constructing `PlaybookAgent` with stub LLMs and asserting on `agent.runtime.state` - slots, checkpoint, ended/outcome. ```python theme={null} import pytest from superdialog.playbook import Playbook, PlaybookAgent, httpx_http @pytest.mark.asyncio async def test_kyc_collects_aadhaar(): agent = PlaybookAgent( playbook=Playbook.load("kyc.yaml"), talker_llm=stub_talker, # scripted StreamsLLM director_llm=stub_director, # scripted CompletesLLM http=httpx_http, ) reply = await agent.turn("My Aadhaar starts with 1234.") assert reply.text assert agent.runtime.state.slot_value("aadhaar_last_4") == "1234" ``` `agent.runtime.state` is the folded `ConversationState`: `slot_value(key)`, `confirmed(keys)`, `checkpoint_id`, `ended`, `outcome`. ## Live smoke test through the entry point For an end-to-end check against a real model, use the public entry point with a cheap, fast model: ```python theme={null} import pytest from superdialog import DialogMachine @pytest.mark.asyncio async def test_greets_customer(): agent = DialogMachine("kyc.yaml", llm="anthropic/claude-haiku-4-5") reply = await agent.turn("Hello") assert reply.text # non-empty response @pytest.mark.asyncio async def test_kyc_collects_aadhaar_live(): agent = DialogMachine("kyc.yaml", llm="anthropic/claude-haiku-4-5") await agent.turn("I need to verify my KYC.") reply = await agent.turn("My Aadhaar ends in 1234.") state = agent.state # {"checkpoint": ..., "slots": ..., "ended": ...} assert "1234" in reply.text or state["slots"].get("aadhaar_last_4") == "1234" ``` ## Replay - regression without re-running models Because the event log is the source of truth, you can re-run the Director over a recorded session under a (possibly edited) playbook and diff its decisions - LLM-free regression evidence for prompt or model changes: ```python theme={null} from superdialog.playbook import EventLog, Playbook, replay log = EventLog.from_jsonl(open("session.jsonl").read()) report = await replay(log, Playbook.load("kyc.yaml"), director_llm) assert report.stable # every replayed decision matched the recording ``` ## Persona evals Drive scripted personas through a fresh agent per run and score completion, slot accuracy, and turns-per-checkpoint: ```python theme={null} from superdialog.playbook import PersonaSpec, run_eval personas = [PersonaSpec( name="impatient", traits="gives all details at once", goal="verify KYC", max_turns=10, opening="Hi", ground_truth_slots={"aadhaar_last_4": "1234"}, )] report = await run_eval( playbook_factory=lambda: make_agent(), personas=personas, user_llm=user_llm, n=1, ) assert report.completion_rate == 1.0 assert report.mean_slot_accuracy > 0.9 ``` The CLI wraps the same loop: `superdialog optimize --playbook kyc.yaml` runs paired evals and proposes prose-only improvements. ## Testing tools ```python theme={null} from superdialog.playbook import Playbook, PlaybookAgent, httpx_http @pytest.mark.asyncio async def test_tool_is_called(): calls = [] async def lookup_customer(args, state) -> dict: """Look up customer by ID.""" calls.append(args["customer_id"]) return {"name": "Ravi Kumar", "verified": True} agent = PlaybookAgent( playbook=Playbook.load("kyc.yaml"), talker_llm=stub_talker, director_llm=stub_director, http=httpx_http, python_tools={"lookup_customer": lookup_customer}, ) await agent.turn("My customer ID is CUST-999.") assert "CUST-999" in calls ``` ## Legacy graph engine Same pattern with `engine="flow"`, asserting on the machine's `state` property (which returns `{"node_id": ..., "slots": ...}` on the graph engine): ```python theme={null} import pytest from superdialog import DialogMachine, Flow, FlowSet @pytest.mark.asyncio async def test_kyc_collects_aadhaar_flow(): dm = DialogMachine(Flow.load("kyc.json"), llm="anthropic/claude-haiku-4-5", engine="flow") await dm.turn("My Aadhaar ends in 1234.") assert dm.state["slots"].get("aadhaar_last_4") == "1234" @pytest.mark.asyncio async def test_switch_to_escalation(): flowset = FlowSet({"main": main_flow, "escalation": escalation_flow}) dm = DialogMachine(flowset, llm="anthropic/claude-haiku-4-5", engine="flow") dm.switch_flow("escalation") reply = await dm.turn("I want to speak to a manager.") assert "escalat" in reply.text.lower() or "manager" in reply.text.lower() ``` Set `traversal_dir` on a graph-engine machine to capture each completed session as JSON for an eval corpus. ## Tips * Use a cheap model (`claude-haiku-4-5`) in live tests to keep costs and latency low * Use scripted Talker/Director LLMs for deterministic, offline assertions * Keep playbook YAML / flow JSON in version control so tests are reproducible * Build a corpus from recorded event logs, then use `replay` / `run_eval` / `superdialog eval` for regression # Unpod (hosted voice) Source: https://docs.unpod.ai/superdialog/embedding-guides/unpod-voice Connect a SuperDialog dialog machine to Unpod voice calls via the unpod SDK. ## How it works The `unpod` SDK connects your `AgentRunner` to Unpod's voice platform over WebSocket. Unpod handles STT, TTS, telephony, numbers, and recording - your code handles dialog logic using a SuperDialog `DialogMachine` or `LLMAgent`. Animated SuperDialog and Unpod voice integration diagram showing caller speech through STT, user-turn hooks, DialogMachine.turn, agent-turn hooks, TTS, and caller playback. Your dialog machine runs inside your process, in the same call as the rest of your agent logic. No separate WebSocket server needed. *** ## Step 1 - Build your dialog machine ```python theme={null} from superdialog import DialogMachine, PythonTool def lookup_customer(phone: str) -> dict: """Look up customer by phone number.""" return crm.get_by_phone(phone) dialog_machine = DialogMachine( "kyc.yaml", # any format; Playbook engine by default llm="anthropic/claude-haiku-4-5", tools=[PythonTool.of(lookup_customer)], ) ``` See [SuperDialog Quickstart](/superdialog/quickstart) to generate and save a playbook. *** ## Step 2 - Plug the machine into your session Assign `dialog_machine` to `ctx.session.dialog_machine` inside your `AgentRunner` entrypoint. The SDK auto-wraps it - no adapter import needed. ```python theme={null} from unpod import AgentRunner, CallContext from superdialog import DialogMachine, Flow, PythonTool def lookup_customer(phone: str) -> dict: """Look up customer by phone number.""" return crm.get_by_phone(phone) async def handle_call(ctx: CallContext) -> None: # Build (or re-use) the agent per call machine = DialogMachine( "kyc.yaml", llm="anthropic/claude-haiku-4-5", tools=[PythonTool.of(lookup_customer)], ) ctx.session.dialog_machine = machine # auto-wrapped by SuperDialogAdapter await ctx.session.run() # blocks until the call ends AgentRunner( entrypoint=handle_call, agent_id="kyc-bot", ).start() ``` *** ## Step 3 - Register a Speech Pipe (once) A Speech Pipe is the voice front-end calls arrive on. Create one, attach a number, and give it the **same `agent_id`** your `AgentRunner` uses - a mismatch is the most common first-run failure. ```python theme={null} pipe = await client.pipes.create( name="KYC Bot", voice_profile=profiles[0].profile_id, agent_id="kyc-bot", # must match AgentRunner(agent_id=...) recording=True, ) await client.numbers.attach(number_id=numbers[0].number_id, pipe_id=pipe.pipe_id) ``` Full script, env vars, and voice-profile lookup: [Provisioning checklist](/speech-stack/setup-checklist). *** ## Complete example The long-lived runner process. Pipe and number are provisioned once beforehand * see [Provisioning checklist](/speech-stack/setup-checklist). ```python theme={null} from superdialog import DialogMachine, PythonTool from unpod import AgentRunner, CallContext # --- Tools (playbook built with `superdialog generate`) --- def lookup_aadhaar(partial: str) -> dict: """Look up customer by partial Aadhaar.""" return crm.lookup_by_partial_aadhaar(partial) # --- Runner (long-lived process) --- async def handle_call(ctx: CallContext) -> None: machine = DialogMachine( "kyc.yaml", llm="anthropic/claude-haiku-4-5", tools=[PythonTool.of(lookup_aadhaar)], ) ctx.session.dialog_machine = machine await ctx.session.run() AgentRunner( entrypoint=handle_call, agent_id="kyc-bot", ).start() ``` *** ## Using pre-call data in the flow Data passed when triggering an outbound call (or injected by the platform) is available on `ctx.data`: ```python theme={null} async def handle_call(ctx: CallContext) -> None: machine = DialogMachine("onboarding.yaml", llm="anthropic/claude-haiku-4-5") # Inject caller context before the first turn if customer_name := ctx.data.get("customer_name"): machine.assist(f"The customer's name is {customer_name}. Address them by name.") ctx.session.dialog_machine = machine await ctx.session.run() ``` *** ## Mid-call context injection Inject system instructions at any point during an active call from your own business logic: ```python theme={null} async def handle_call(ctx: CallContext) -> None: machine = DialogMachine("support.yaml", llm="openai/gpt-4.1-mini") @ctx.session.on("user_turn") async def _(text: str) -> None: sentiment = await analyze_sentiment(text) if sentiment == "frustrated": machine.assist("The customer seems frustrated. Be empathetic and offer escalation.") ctx.session.dialog_machine = machine await ctx.session.run() ``` *** ## Switching flows mid-call (graph engine) `switch_flow` is a **graph-engine** feature - construct the machine with a `FlowSet` and `engine="flow"`: ```python theme={null} from superdialog import DialogMachine, Flow, FlowSet async def handle_call(ctx: CallContext) -> None: flows = FlowSet({"triage": Flow.load("triage.json"), "billing": Flow.load("billing.json")}) machine = DialogMachine(flows, llm="openai/gpt-4.1-mini", engine="flow") @ctx.session.on("user_turn") async def _(text: str) -> None: if "billing" in text.lower(): machine.switch_flow("billing", preserve_memory=True) ctx.session.dialog_machine = machine await ctx.session.run() ``` On the **Playbook engine** (the default), model the same behaviour with multiple `journeys` and `interrupts` inside a single playbook instead of swapping flows. See [Playbooks](/superdialog/playbooks). *** ## vs. LiveKit / PipeCat adapters | | Unpod Voice (SDK) | LiveKit adapter | PipeCat adapter | | ------------------------- | -------------------------------- | ------------------------- | -------------------------- | | **Who handles STT/TTS** | Unpod | You (via LiveKit plugins) | You (via PipeCat services) | | **Who handles telephony** | Unpod | You / LiveKit SIP | You / Twilio / etc. | | **Dialog runs in** | Your AgentRunner process | Your LiveKit agent | Your PipeCat pipeline | | **Best for** | Fastest path to production voice | Full media layer control | Existing PipeCat pipelines | *** ## Next Steps Constructor, credentials, lifecycle, and live-call controls - say(), transfer(), recording. Author the simple and full playbook formats. Add HTTP, Python, and MCP tools to your agent. # A/B Evals Source: https://docs.unpod.ai/superdialog/evals A/B-evaluate a playbook on the SuperDialog engine against a vanilla LLM handed the same playbook - scored from the transcript alone, so both are judged by an identical rubric. ## What it answers `superdialog eval` answers one question: **does running your playbook on the SuperDialog engine beat handing the same playbook to a raw LLM as a flat system prompt?** It drives two modes over one dataset and scores both from the conversation **transcript only** - never from engine internals - so the playbook and the vanilla baseline face an identical rubric. Use it to justify adopting the engine, to catch regressions, or to expose your playbook to any external benchmark. This is different from `superdialog optimize` and the persona-eval loop in the [API Reference](/superdialog/api-reference#replay-and-the-eval-bridge). That loop asks "is this playbook good enough, and how do I improve its prose?" - the A/B harness here asks "is the playbook **machinery** earning its keep over a plain prompt?" ## The two modes | Mode | What runs | | ---------- | ------------------------------------------------------------------- | | `playbook` | The full Director + Talker checkpoint runtime loading your playbook | | `vanilla` | One raw LLM handed the playbook file as a single flat system prompt | The runner only sees `str` in / `str` out, so a mode is just a factory that returns a conversation endpoint. Endpoints ship for in-process playbook / vanilla, a remote HTTP SuperDialog server, and any OpenAI-compatible model. ## Headline metrics | Metric | Kind | Reads | | --------------- | --------------------- | ------------------------------------------------------------------------------ | | `task_success` | LLM judge (0–1) | full transcript vs the case goal | | `slot_accuracy` | LLM judge (0–1) | transcript vs `ground_truth_slots` | | `guardrail` | LLM judge (hard gate) | each guardrail-probe reply | | `efficiency` | pure code | user turns + assistant latency p50/p95 | | `token_cost` | pure code | input tokens per assistant turn, with a director/talker split in playbook mode | `guardrail` is a **hard gate**. If either mode complies with a probe attack, that case's composite score is zeroed and it counts toward the `guardrail_violation_rate` - no matter how well it did on the other metrics. `efficiency` and `token_cost` are pure code - no judge tokens, no added latency - so every run includes them regardless of `--metrics`. The report gains a **Latency & tokens** table (p50/p95, input tok/turn, director/talker split, LLM calls/turn) and a **framework** score: zero unless quality is perfect (`task_success=1`, `slot_accuracy=1`, guardrail clean), then higher for lower latency and fewer tokens - the framework's goal as one number. ## Run it Two phases: build the dataset once (offline, commit it), then A/B-run it. ```bash theme={null} # 1. Build .evalcases.yaml - personas auto-generated, probes injected superdialog eval gen-dataset --playbook spa.yaml --n-probes 8 # 2. A/B both modes → report.json (full) + report.md (headline table + drilldown) superdialog eval run \ --playbook spa.yaml --dataset spa.evalcases.yaml \ --modes vanilla,playbook \ --agent-model openai/gpt-4.1-mini \ --judge-model openai/gpt-4.1-mini \ --metrics task_success,slot_accuracy,guardrail,efficiency \ --out ./eval-out ``` Useful `eval run` flags: | Flag | Default | Description | | ------------------------------------- | --------------------- | --------------------------------------- | | `--modes` | `vanilla,playbook` | Which modes to compare | | `--agent-model` | `openai/gpt-4.1-mini` | Model both modes answer with | | `--director-model` / `--talker-model` | agent model | Per-role LLMs for playbook mode | | `--judge-model` | `openai/gpt-4.1-mini` | LLM that scores the transcripts | | `--user-model` | agent model | LLM that simulates the persona / caller | | `--metrics` | all four | Comma-separated headline metrics | | `--repeats` | `1` | Runs per case (average out variance) | The dataset format mirrors the RAGAS single-/multi-turn shape: each case carries a persona, `ground_truth_slots`, and a list of probes. A persona may also define `afterlife_probes` - utterances sent *after* the session ends, asserted on by the suite runner's `silent_afterlife` check (above). Or run both phases in one shot with `eval bench` - it builds the dataset if missing (`--regen` rebuilds, `--personas` seeds), A/Bs every `--models` entry into its own report directory, and adds `--max-turns` to override each persona's turn budget: ```bash theme={null} superdialog eval bench --playbook spa.yaml \ --models openai/gpt-4o-mini --max-turns 20 --out ./eval-out ``` ## Gate a suite in CI `eval run` and `eval bench` produce scores you eyeball. `superdialog eval suite` turns a set of benches into a one-command, CI-able **behavioral regression gate**: each suite pins a playbook, dataset, and models to the *expectations* that made the run worth doing (this case must fire the goodbye interrupt, that control must **not**; this case must not answer after it ended). ```bash theme={null} superdialog eval suite --config suites.yaml --tier smoke # cheap pre-merge signal superdialog eval suite --config suites.yaml --tier full # run everything superdialog eval suite --config suites.yaml --only realestate-disconnect --force ``` The registry is YAML - one entry per suite: ```yaml theme={null} suites: - name: realestate-disconnect playbook: examples/playbooks/realestate_site_visit.simple.yaml dataset: examples/datasets/realestate_disconnect.evalcases.yaml models: [livekit/google/gemma-4-31b-it] judge: openai/gpt-4.1-mini user_model: openai/gpt-4.1-mini fallback_judge: livekit/openai/gpt-4o-mini # retried on provider quota (429) fallback_user: livekit/openai/gpt-4o-mini max_turns: 14 smoke_cases: [explicit-disconnect-mid, objecting-but-staying-pooja] min_composite: 0.6 expect: explicit-disconnect-mid: {goodbye: fired, min_task_success: 0.7} objecting-but-staying-pooja: {goodbye: absent, min_turns: 4} ``` Each `expect` entry asserts one case's behavior from the run's report and log: | Expectation | Asserts | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `goodbye` | `fired` / `absent` - did the goodbye interrupt fire (matched via `goodbye_signal`, default `interrupt:global_goodbye`) | | `min_turns` | Conversation length floor - encodes "not ended prematurely" for a control persona who legitimately closes once finished | | `no_reentry` | Route integrity - no checkpoint is re-entered after the flow moved past it (a restart/regression). Skip on playbooks with `resume: true` interrupts | | `silent_afterlife` | Post-end **afterlife probes** (utterances sent *after* the session ended) draw **no** reply - an ended session that answers is a zombie. Requires the persona to define `afterlife_probes` | | `no_repeat_replies` | No assistant reply exactly repeats the previous one (closing/parrot loops) | | `min_task_success` | Per-case `task_success` floor | | `min_composite` | Suite-level composite-mean floor | **Tiers, skip-if-unchanged, and quota fallback.** `--tier smoke` runs only each suite's `smoke_cases`; `--tier full` runs everything. A content hash of playbook + dataset + params is stamped in the out dir, so unchanged suites are skipped unless `--force`. A run that dies on provider quota (`insufficient_quota` / `429`) is retried once with the suite's `fallback_judge` / `fallback_user`; **behavioral** checks (goodbye, route, afterlife, repeats) always gate, but score floors (`task_success`, `composite`) downgrade to **advisory** under the fallback judge since they were calibrated against the primary one. The command exits non-zero if any suite fails or errors. ## Serve it to an external benchmark Expose the playbook as an OpenAI-compatible endpoint and grade it like any other model: ```bash theme={null} superdialog eval serve --playbook spa.yaml --port 8000 # POST /v1/chat/completions → the playbook answers as the "model" ``` ## RAGAS is optional (and version-pinned) The custom LLM judges produce every headline metric with **no RAGAS installed**. RAGAS metrics are opt-in via the `ragas` extra: ```bash theme={null} pip install superdialog[ragas] ``` SuperDialog ships **two** RAGAS-based harnesses on incompatible RAGAS lines: the A/B `ragas` extra (RAGAS 0.4.3) and the separate `benchmark` extra (RAGAS 0.2.x, used by `superdialog benchmark`). They **cannot co-install** - pick one extra per environment. With `uv`, they are declared conflicting so the project still resolves; with `pip`, install only one. ## Legacy session audit The older single-session audit lives under the same command group: ```bash theme={null} superdialog eval flow --flow kyc.json --traversal session.json ``` See the [CLI Reference](/superdialog/cli#superdialog-eval) for every `eval` subcommand. # Flows (legacy) Source: https://docs.unpod.ai/superdialog/flows Author flow graphs - the legacy / compliance path. Flows run compiled on the Playbook engine by default; opt into the original graph runtime with engine="flow". **Flows are the legacy authoring surface.** The default engine is the [Playbook](/superdialog/playbooks) runtime, and a flow graph **runs compiled onto it by default** - `Playbook.load` detects flow JSON and converts it (`compile_flow`). Reach for a hand-authored graph when you need an enumerable, lintable spec for **compliance** or **strict determinism**. New conversational agents should start with [Thinking in Playbooks](/superdialog/thinking-in-playbooks). ## What is a Flow? A `Flow` is a directed graph: nodes (states) connected by edges (transitions), with metadata at each node (prompts, tool references, slot definitions). It's the static definition of what your dialog can do. By default, `DialogMachine` runs a flow **compiled onto the Playbook engine** - you pass the flow and get checkpoint semantics for free. To run the **original graph runtime** (one `turn()` at a time, every transition authored), pass `engine="flow"`: ```python theme={null} from superdialog import DialogMachine, Flow # Default: flow JSON is compiled and run on the Playbook engine agent = DialogMachine(Flow.load("kyc.json"), llm="anthropic/claude-haiku-4-5") # Legacy graph runtime (opt-in) dm = DialogMachine(Flow.load("kyc.json"), llm="anthropic/claude-haiku-4-5", engine="flow") ``` SuperDialog flow graph diagram showing states connected by conditional edges, including fallback and retry paths. ## Building from a prompt The fastest way to get started. `create_dialog_flow` makes one LLM call and generates the graph for you. ```python theme={null} import asyncio from superdialog import create_dialog_flow flow = asyncio.run(create_dialog_flow( prompt="Confirm KYC. Ask the customer for the last 4 digits of their Aadhaar. Confirm their date of birth. Thank them on completion.", llm="openai/gpt-5.1", )) flow.save("kyc.json") ``` Tips for prompts: * Describe the goal and each step in plain language * Mention what data you need to collect (slots) * Describe any branching logic ("if they decline, escalate to an agent") ## Building by hand For precise control over graph structure, construct nodes and edges directly: ```python theme={null} from superdialog.flow import Flow, Node, Edge flow = Flow( nodes=[ Node(id="greet", prompt="Greet the customer and ask how you can help."), Node(id="collect_name", prompt="Ask for the customer's full name."), Node(id="done", prompt="Thank the customer and confirm the details.", terminal=True), ], edges=[ Edge(from_node="greet", to_node="collect_name", condition="customer_responded"), Edge(from_node="collect_name", to_node="done", condition="name_collected"), ], ) flow.save("manual.json") ``` ## Saving and loading Flows support JSON and YAML formats - commit them to source control alongside your code. ```python theme={null} from superdialog import Flow # Save as JSON flow.save("flows/kyc.json") # Load - auto-detects format from extension flow = Flow.load("flows/kyc.json") flow = Flow.load("flows/kyc.yaml") # Explicit format loaders flow = Flow.from_json_file("flows/kyc.json") flow = Flow.from_yaml_file("flows/kyc.yaml") # Load from string flow = Flow.from_json_string(json_str) flow = Flow.from_yaml_string(yaml_str) # Load from dict flow = Flow.from_config(config_dict) ``` ### React Flow editor support Flows exported from the React Flow visual editor (camelCase JSON) are automatically detected and normalized: ```python theme={null} # Works directly - no manual conversion needed flow = Flow.load("flows/kyc-react-flow-export.json") ``` ## Multiple flows with FlowSet A `FlowSet` holds several named flows. Use it when a conversation may branch across distinct sub-flows (e.g. main flow → escalation, billing, or FAQ). `FlowSet` and `switch_flow` are **graph-engine features** - construct the machine with `engine="flow"`. On the Playbook engine, use multiple `journeys` and advance rules instead (see [Playbooks](/superdialog/playbooks)). ```python theme={null} from superdialog import FlowSet, DialogMachine flowset = FlowSet({ "main": main_flow, "escalation": escalation_flow, "billing": billing_flow, }) dm = DialogMachine(flowset, llm="openai/gpt-4.1-mini", engine="flow") ``` Switch flows at runtime: ```python theme={null} # Reset state on switch (default) dm.switch_flow("escalation") # Keep conversation history dm.switch_flow("billing", preserve_memory=True) ``` ## Validating and inspecting flows Use the legacy `flow` CLI sub-tree to lint and visualise a flow graph: ```bash theme={null} # Check graph structure for errors superdialog flow lint kyc.json # Render a Mermaid diagram superdialog flow draw kyc.json # Re-generate a flow graph from a prompt (legacy; `superdialog generate` # writes a playbook instead) superdialog flow generate "Confirm KYC." --llm openai/gpt-5.1 --output kyc.json # Run the original graph runtime in the REPL superdialog chat kyc.json --mode flow ``` ## Migrating a flow to a playbook A flow already runs on the Playbook engine by default. To make the conversion explicit - and to keep authoring in the playbook format going forward - compile it: ```bash theme={null} # Compile flow JSON to a playbook YAML you can edit superdialog playbook compile kyc.json # Or compile-and-run in one step superdialog playbook run kyc.json ``` ```python theme={null} from superdialog import Flow from superdialog.playbook import compile_flow, coverage_report flow = Flow.load("kyc.json") pb = compile_flow(flow) # single-journey "main" playbook report = coverage_report(flow, pb) # lossless proof assert not report.unmapped_nodes assert not report.unmapped_edges assert not report.unmapped_actions ``` `compile_flow` is lossless by construction; `coverage_report` lists anything that didn't map (any entry is a compiler bug). Run it in CI next to the compiled artifact. See the [API Reference](/superdialog/api-reference#migrating-flows) for the full mapping table. ## Flow versioning Because flows are plain JSON files, they work naturally with git: ```bash theme={null} git diff flows/kyc.json # see what changed git log flows/kyc.json # see history git blame flows/kyc.json # see who changed what ``` Pin a specific flow version by checking out a commit hash - useful for A/B testing different flow designs against the same eval corpus. # What is SuperDialog? Source: https://docs.unpod.ai/superdialog/introduction A conversation framework with two engines behind one Agent protocol - the Playbook engine (default) for fluid conversations, and the legacy DialogMachine graph runtime. Pure text in, pure text out. ## Overview **`pip install superdialog` - no account, no API key.** SuperDialog is an open-source Python framework that runs in your own process. Unpod's hosted voice is one place to run it, alongside LiveKit, Pipecat, and FastAPI. It is the **brain** layer for conversational systems: it takes a prompt or an authored artifact and turns it into a running conversation runtime - managing turn-by-turn logic, tool calls, outcome tracking, and conversation memory. Animated SuperDialog text loop diagram showing user text entering the Agent protocol, agent.turn using tools and state, and reply text coming back. It ships **two engines behind one `Agent` protocol**, and the **Playbook engine is the default everywhere**: * **Playbook engine (default)** - checkpoints gate *outcomes*, not utterances. A fast Talker streams every spoken turn while an async Director extracts data, judges progress, and runs tools over an event-sourced log. This is where new investment goes. * **DialogMachine (supported legacy)** - the graph-railed state machine: nodes, edges, and criteria, where every transition is authored. Still fully supported, opt-in via `engine="flow"`. Existing flow graphs run **compiled on the Playbook engine by default**, so nothing breaks. Turn ordering, the event-sourced log, gates, and degradation are covered in [Architecture](/superdialog/architecture). It is intentionally narrow in scope. Audio, STT, TTS, telephony, and media servers are all out of scope - those belong to voice infrastructure like LiveKit, PipeCat, or the Unpod Voice Platform. SuperDialog ends at text in, text out - on both engines. Read the mental-model guide before diving into the quickstart. Browse the source, issues, and releases at `unpod-ai/superdialog`. **Coming from the Speech Stack?** Assign your SuperDialog agent to `ctx.session.dialog_machine` and the SDK wraps it for you - see [Run a SuperDialog agent](/speech-stack/level-up-superdialog). ## Why SuperDialog exists ### The brain has natural reuse beyond voice A conversation brain that runs a customer-onboarding journey works the same whether the user is on a phone, a WhatsApp thread, an Intercom widget, or a CLI test harness. Coupling it to telephony forecloses every non-voice use case. ### The dependency direction matters Voice infrastructure should depend on SuperDialog (as one brain option), not the other way around. A modular architecture keeps the framework portable and the platform composable. ## Who it's for | Audience | Why they care | | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Voice developer using LiveKit / PipeCat** | Drop SuperDialog in as the brain; `PlaybookAgent` gives real token streaming through the same adapters | | **Chatbot developer (text-only)** | `superdialog generate` a playbook, chat against it from the CLI, embed it with FastAPI through the `Agent` protocol | | **Developer with compliance / scripted flows** | Author the flow graph as the spec - every path enumerable and lintable - and run it compiled on the Playbook engine, or on DialogMachine via `--mode flow` | | **Enterprise dev with a custom LLM** | Plug any LLM URI and get the full framework for free | | **Unpod Voice Platform customer** | SuperDialog is the default brain Unpod offers - same code runs locally and in Unpod cloud | ## How it compares SuperDialog is to **conversation flow** what n8n is to **integration workflow** - a simple, composable, eval-able runtime for orchestrating turn-by-turn logic. Where LangChain and LangGraph expose general agent primitives, SuperDialog focuses narrowly on the conversational core: who speaks next, what to say while tools run, which checkpoint or flow the conversation is in, when to call a tool, when to escalate, and which outcome the session ended with. The pitch: *"if your problem is conversation state, this is the right size."* ## Two engines, one entry point `DialogMachine` is the recommended way in. It runs the Playbook engine by default; pass `engine="flow"` for the legacy graph runtime. Both engines sit behind the same `Agent` protocol, so sessions and host adapters run either one unchanged. ```python theme={null} from superdialog import DialogMachine agent = DialogMachine("booking.yaml", llm="openai/gpt-4.1-mini") result = await agent.turn("hello") ``` Playbook is the default because users don't follow graphs: the graph-railed model gated every utterance and still cost two serial LLM calls per turn. Checkpoints gate outcomes instead - the model owns the phrasing, the framework owns "done". **Existing flows are migrated, not replaced**: `Playbook.load` detects flow JSON and compiles it (`compile_flow`), with `coverage_report` proving every node, edge, and action mapped. Side-by-side comparison, and when a graph still fits: [Thinking in Playbooks](/superdialog/thinking-in-playbooks). ## What it explicitly is not * **Not a UI flow designer** - that belongs to a downstream tool * **Not a voice framework** - audio, STT, TTS are out of scope (the Talker streams text tokens; the host turns them into speech) * **Not multi-modal** - text only at the interface (vision/audio via tools if needed) * **Not a hosted service** - SuperDialog is a library; the Unpod Voice Platform provides hosting for those who want it # Playbooks Source: https://docs.unpod.ai/superdialog/playbooks Author playbooks - the simple format to start, the full format when you need typed slots, gates, pipelines, and multiple journeys. ## What is a playbook? A **playbook** is the authored, git-diffable artifact the Playbook engine runs. It has two layers: * **Conversation layer** - `journeys` of **checkpoints** (a goal, typed slots, guidance prose, and ordered advance rules) plus a `persona`. * **Process layer** - everything that isn't conversation: `tools`, `pipelines`, `handlers`, `interrupts`, and `policies`. There are **two authoring formats and one engine**. Start in the simple format; graduate to the full format when you need precision. Both compile to the same validated artifact and run identically. Animated Playbook loading pipeline diagram showing simple YAML, full YAML, and legacy flow JSON converging into a validated Playbook artifact and one runtime. `Playbook.load(path)` auto-detects all three, so callers never branch on format. ## The simple format Prose steps, a structured persona, and reference data as real YAML. This is what `superdialog generate` writes. ```yaml theme={null} goal: "Book a haircut and confirm it." persona: name: Mira language: ["en", "hi"] voice_style: "Warm and brief. One question at a time." identity: "You are Mira, a booking assistant for Glow Studio." opening: "Greet the caller warmly." closing: "Thank them and say goodbye." playbook: - id: greet purpose: "Open the call." say: "Greet the caller and ask how you can help." done_when: "Caller is ready to book." - id: collect purpose: "Get the booking details." say: "Ask for their name and preferred service." collect: [name, service] done_when: "Name and service are captured." - id: confirm purpose: "Confirm and close." say: "Read back the booking and confirm." done_when: "Caller has confirmed." facts: canonical_pricing: {haircut: "₹400"} boundaries: ["NEVER invent prices."] interrupts: - {when: "Caller says goodbye or asks to end the call.", to: main.confirm} ``` ### Section reference | Key | Meaning | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `goal` | The call's mission statement - what makes this call a win. | | `persona` | `identity` (who the agent is), `name`, `language` (first is default), `voice_style` (tone, pacing). Compiles into the persona the Talker sees every turn. | | `opening` / `closing` | Optional greeting / sign-off prose. `opening` seeds the first step's guidance when it has no `say`. | | `playbook` | Ordered steps. Each becomes a checkpoint in a single journey `main`, **chained linearly by default**: step N's `done_when` advances to step N+1. Override with `then:` (explicit target) or `branches:` (conditional routing); reordering the list re-wires the default chain. | | `facts` | Grounding data (pricing, policies) the agent may recite - never invent beyond it. | | `objections` | `{trigger, handle}` steering, handled *within* a step. | | `boundaries` | Compliance "NEVER…" rules (prose-enforced). | | `interrupts` | Global jumps judged from any step (`{when, to}`). | | `fallback_actions` | Ordered fallback steering when the caller stalls or goes off-script. | Also valid at the top level: `name`, `channel`, `tone`, `call_type`, `timezone`, `memory_enabled`, `followup_enabled`, and the multi-entity toggles `multi_entity` / `supervisor`. **Any key not in this set raises at load** (see [Strict validation](#strict-validation)). Per step: | Field | Meaning | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | Checkpoint id; addressable as `main.` in logs, metrics, replay. | | `purpose` | The goal (Director-facing). | | `say` | Guidance the Talker speaks from. | | `collect` | Slot keys to capture. | | `done_when` | Observable condition that advances (one `judge: llm` rule). | | `require` | Explicit subset of `collect` that gates the advance. | | `turn_budget` | User turns before a "wrap this step up" nudge (default 4). | | `kb` | Whether this step's prompt carries the knowledge base; set `false` on steps that only mention it. | | `then` | Step id to advance to when `done_when` holds, instead of the next list element. Unknown targets and self-targets are rejected at load. | | `terminal` + `outcome` | Mark the step as a call ending (default `outcome: closed`), recorded on `SessionEnd`. Any step can be terminal now - not only the last. `outcome` on a non-terminal step is rejected. | | `branches` | Multi-way routing, judged in author order **ahead of** `done_when`. Each entry is `{when, to, requires?}` - note the target key is `to`, not `then`. A branch may not target the default next step or itself; terminal steps cannot have branches. | | `then_say` | A line delivered *while advancing out of* this step (post-capture pitch), rendered Jinja over slots. Guidance written after the capture in `say` is unreachable, so use `then_say`. Never spoken on interrupt/policy advances; rejected on terminal steps. | | `entity` / `gate` | Advanced: turn owner (`caller`/`agent`) and per-step `soft`/`hard` gate. | **Routing example** - a branch off the default chain, and two terminal outcomes: ```yaml theme={null} playbook: - id: pitch_visit say: "Offer the site visit; handle the objection on hesitation." done_when: "Customer accepts or is open to the visit." then_say: "Great - let me lock that in." # spoken while advancing out branches: - when: "Customer firmly declines after the objection was handled." to: advisor_callback # `to`, not `then`; never the default next step - id: book_visit # pitch_visit's default next step say: "Confirm the slot, read it back, and say goodbye." terminal: true # a terminal step need not be the last one outcome: visit_booked - id: advisor_callback say: "Capture a callback time." collect: [callback_time] done_when: "Callback time captured." - id: close # advisor_callback's default next step say: "Thank them and say goodbye." terminal: true outcome: callback_scheduled ``` **Which `collect` keys gate advancement depends on the step's shape.** A focused capture step (≤2 slots) requires all of them filled before the Director may advance; a branchy step collecting more than 2 per-path alternatives requires **none** - demanding every slot of a 14-slot category qualifier would deadlock the step. Use `require:` to override the heuristic (e.g. one mandatory key on an otherwise-branchy step). **Always add a goodbye interrupt.** In testing, linear playbooks with no early exit never completed a single call (a satisfied caller loops until the turn cap); the same playbook with goodbye/busy interrupts completed every call. ### Strict validation Keys the format does not recognize - a typo'd `done_wehn`, an invented top-level `language_lock:` - **raise at load** with the dotted path of every offender, instead of being silently dropped (config theater: you think it's set, the runtime never sees it). For live loaders that must not kill a call over a stale authored file, downgrade to a warning: ```python theme={null} from superdialog.playbook.simple import simple_to_playbook, load_simple pb = simple_to_playbook(doc, strict=False) # warn instead of raise pb = load_simple(path, strict=False) ``` ### What the simple format cannot express Multiple terminals/outcomes, per-step `gate` and `then`/`branches` routing are now all expressible in the simple format (above). When you need any of these, move to the full format: * Pipelines and tools (transactional steps - holds, payments) * `judge: expr` rules (machine-evaluated transitions - zero LLM cost) * Typed/required slots, `never_say`, `say_verbatim`, silence policy, multiple journeys The escape hatch is one-way: compile your simple file and continue authoring the result. There is no decompiler back. ## The full format Everything the engine can do, stated explicitly. The conversation layer is `journeys` of checkpoints; the process layer is `tools`, `pipelines`, `handlers`, `interrupts`, `policies`. ```yaml theme={null} persona: "You are Asha, a friendly golf-course booking assistant." llm: # the model loads from here (top-level persona `llm` is deprecated) provider: anthropic model: claude-haiku-4-5 director: anthropic/claude-haiku-4-5 # optional: separate Director model views: # computed, LLM-free exprs; shown as reference data hold_valid_until: "results.hold.data.valid_until" journeys: booking: checkpoints: - id: collect # addressed as booking.collect goal: "Have city and date" slots: # typed, flow-scoped declarations city: type: str # str|int|float|bool|date|enum|array|object required: true invalidates: [hold] # a city change clears the stale hold result date: {type: date, required: true} players: {type: int} guidance: | # Jinja over {slots, views, results} Collect naturally; the caller may give everything in one breath. never_say: ["our systems are slow"] turn_budget: 6 # steer to wrap up after 6 user turns here on_failure: booking.handoff advance_when: # ordered; first matching rule wins - when: "caller gave the booking details" judge: llm # the Director judges intent to: booking.confirm requires: [city, date] # rule fires only when these are met - id: confirm gate: hard # outcomes barrier on the Director here pipeline: confirm_and_hold # process layer runs on entry advance_when: - {when: "pipeline.ok", judge: expr, to: booking.close} - {when: "pipeline.failed", judge: expr, to: booking.collect} - id: close terminal: true # session ends on entry outcome: confirmed # label for metrics and host hangup - id: handoff terminal: true # the `on_failure` target declared above outcome: escalated tools: - id: hold_slot type: http method: POST url: "{{ env.API_BASE_URL }}/slots/hold" headers: {Authorization: "Bearer {{ env.ACCESS_TOKEN }}"} body: {city: "{{ slots.city }}", date: "{{ slots.date }}"} store_response_as: hold # readable as results.hold.* afterwards pipelines: - id: confirm_and_hold steps: - tool: hold_slot on: ok: continue http_409: booking.collect # typed HTTP-status branch failed: {retry: 1, on_exhaust: booking.collect} interrupts: - {id: goodbye, when: "caller says goodbye", judge: llm, to: booking.close} policies: silence: max_prompts: 2 prompts: ["Can you hear me?", "Are you there?"] then: booking.close hold_timeout: 4.0 # max wait before the hold line is spoken (default 4.0s) filler: "Ek second…" # author barrier line while the Director settles hold_line: "Still working on it, bear with me." # spoken after hold_timeout ``` ### The building blocks | Block | Key fields | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Checkpoint** | `goal`, `slots`, `guidance`, `advance_when`, `gate` (`soft`/`hard`), `say_verbatim`, `never_say`, `exit_say` (post-capture pitch; `then_say` in the simple format), `auto`, `pipeline`, `on_failure`, `terminal` + `outcome`, `turn_budget` | | **SlotSpec** | `type`, `required`, `values` (enum), `authoritative` (tool-written only), `invalidates`, `description` | | **AdvanceRule** | `when` (prose or expr), `judge` (`llm`/`expr`), `to` (checkpoint ref), `requires` (slots that must be filled/confirmed), `set` (slot writes on advance) | | **ToolSpec** | `type` (`http`/`python`), `method`/`url`/`headers`/`body` (sandboxed Jinja), `store_response_as`, `env_updates`, `run_once`, `when`, `timeout` | | **PipelineSpec** | ordered `steps`, each with typed `on: {ok, failed, http_}` branches and capped `retry` | | **LLMConfig** (`llm:`) | `provider`, `model`, optional `director` - the model-loading path (persona-level `llm` is deprecated and warns) | | **Policies** (`policies:`) | `silence`, `hold_timeout` (default 4.0), `filler` + `hold_line` (author-facing barrier lines spoken while the Director settles) | Validation runs on load and raises on unknown checkpoint/pipeline/tool refs, duplicate ids, undeclared `requires` keys, and the reserved `pipeline` result key - typos fail fast, not mid-call. **`judge: expr` rules are evaluated LLM-free** at every quiescence hop - this is what makes compiled router chains instant. The expr language is a sandboxed, AST-whitelisted subset of Python over `slots`, `results`, `env`, and `pipeline`. See the [API Reference](/superdialog/api-reference#the-expr-language). ## Generate, then refine ```bash theme={null} # Generate a simple-format playbook from a prompt superdialog generate "Book a tee time. Collect city, date, party size. \ Confirm before holding the slot." --output booking.yaml # Chat against it - watch checkpoints advance superdialog chat booking.yaml # Close the loop: paired persona evals score prose-only edits, output stays # in your source format superdialog optimize --playbook booking.yaml ``` ## How it runs This page is about what you *write*. For what *happens* - the Talker/Director compound runtime, gating semantics, and the event log - see [Architecture](/superdialog/architecture). The mental model is in [Thinking in Playbooks](/superdialog/thinking-in-playbooks). The runtime that executes a playbook The process layer in depth Every field and the expr language Graph authoring and migration # Quickstart Source: https://docs.unpod.ai/superdialog/quickstart Install SuperDialog, generate a playbook from a prompt, and run your first conversation in under 5 minutes. ## Install ```bash theme={null} pip install superdialog ``` Source on GitHub: [unpod-ai/superdialog](https://github.com/unpod-ai/superdialog). Install only the extras you need: ```bash theme={null} pip install superdialog[livekit] # LiveKit adapter pip install superdialog[pipecat] # PipeCat adapter pip install superdialog[fastapi] # FastAPI adapter + uvicorn pip install superdialog[ws] # WebSocket runner pip install superdialog[mcp] # MCP tool support pip install superdialog[langchain] # LangChainAgent ``` ## Step 1 - Generate a playbook from a prompt `superdialog generate` is the default creation path. It writes a validated **simple-format playbook** - prose steps plus a persona - that runs on the Playbook engine. ```bash theme={null} superdialog generate "Confirm a customer appointment. Ask if Friday 4pm works; \ offer 5pm as an alternative if not. Confirm before saving." \ --output appointment.yaml ``` The result is a human-readable, git-diffable YAML file you can edit by hand: ```yaml theme={null} goal: "Confirm the appointment and lock in a time." persona: name: Ava voice_style: "Warm and brief. One question at a time." identity: "You are Ava, a scheduling assistant." playbook: - id: greet purpose: "Open the call." say: "Greet the customer and confirm you're calling about their appointment." done_when: "Customer is ready to confirm a time." - id: confirm_time purpose: "Lock in the slot." say: "Ask if Friday 4pm works; offer 5pm if not." collect: [chosen_time] done_when: "Customer has agreed to a time." ``` Prefer Python? `from superdialog.playbook import generate_simple_playbook` returns the same validated YAML from an async call. ## Step 2 - Build the runtime agent `DialogMachine` is the one entry point. Point it at your artifact and pick a model URI - it runs the Playbook engine by default. ```python theme={null} from superdialog import DialogMachine agent = DialogMachine( "appointment.yaml", # any format: playbook, simple, or legacy flow JSON llm="anthropic/claude-haiku-4-5", # fast model for the speaking turn ) ``` ## Step 3 - Run a conversation ```python theme={null} import asyncio async def chat(): reply = await agent.turn("Hello, I'm calling about my appointment.") print(reply.text) reply = await agent.turn("Friday 4pm works for me.") print(reply.text) asyncio.run(chat()) ``` Or use the bundled CLI - no Python code needed: ```bash theme={null} superdialog chat appointment.yaml ``` The REPL runs on the Playbook engine and prints a per-turn status line (`[checkpoint= ended=]`) so you can watch outcomes advance. ## Step 4 - Add a tool On the Playbook engine, tools live in the playbook's **process layer** - HTTP or registered Python callables the Director runs off the speech path. For a quick local function, register it by id: ```python theme={null} from superdialog import DialogMachine async def lookup_customer(args, state) -> dict: """Look up customer record by phone number.""" return await crm.get_by_phone(args["phone_number"]) agent = DialogMachine( "appointment.yaml", llm="anthropic/claude-haiku-4-5", tools=[...], # see the Tools guide for HTTP / Python / MCP shapes ) ``` See [Tools](/superdialog/tools) for declaring tools in the playbook, pipelines, and the legacy `DialogMachine(tools=[...])` bridge. ## Step 5 - Deploy anywhere The same `agent` object drops into every host - the `Agent` protocol is the only contract: Plug in as an `Agent(llm=...)` plugin - real token streaming Drop in as a `FrameProcessor` in your pipeline Mount a `/turn` endpoint for text chatbots Connect via `WebSocketRunner` to Unpod infrastructure **Prefer a graph?** The legacy path still works: `superdialog flow generate "..." --output appointment.json` writes a flow graph, and `DialogMachine(Flow.load("appointment.json"), llm=..., engine="flow")` runs the original graph engine. See [Flows](/superdialog/flows). By default a flow JSON runs compiled on the Playbook engine - no `engine="flow"` needed. ## What's next The checkpoint mental model Author the simple and full formats Two engines, the Talker/Director runtime, and data flow All `superdialog` commands # Sessions Source: https://docs.unpod.ai/superdialog/sessions Manage multiple concurrent conversations, persist state across process restarts, and use non-DM agent brains. ## When do you need sessions? Sessions add a lifecycle and persistence layer on top of **any `Agent`-protocol brain** - the default `PlaybookAgent` and the legacy `DialogMachine` alike. A bare agent holds its conversation state in memory for the lifetime of the instance. This works perfectly for: * Voice calls (one machine per call, ephemeral) * CLI testing (single conversation, short-lived) * Simple single-user demos You need `SessionWorker` when: * **Multi-user:** multiple concurrent conversations hit the same process * **Multi-worker:** requests are load-balanced across FastAPI workers or pods * **Long-lived chat:** conversations span hours or days and must survive restarts ## The Agent Protocol `SessionWorker` works with any brain that implements this Protocol: ```python Signature theme={null} class Agent(Protocol): async def turn(text: str, *, stream: bool = False) -> TurnResult | AsyncIterator[StreamChunk] def assist(text: str) -> None @property def chat_ctx(self) -> ChatContext def load_chat_ctx(ctx: ChatContext) -> None ``` The Playbook engine's `PlaybookAgent`, the legacy `DialogMachine`, `LLMAgent`, and `LangChainAgent` all implement this. ## SessionWorker The `agent_factory` returns a fresh agent per session. Point `DialogMachine` at a playbook and you get the default engine; the factory is identical whatever the artifact: ```python theme={null} from superdialog import DialogMachine, SessionWorker, InMemorySessionStore worker = SessionWorker( agent_factory=lambda: DialogMachine("kyc.yaml", llm="openai/gpt-4.1-mini"), store=InMemorySessionStore(), max_sessions=1000, # optional cap ) ``` Use it with an async context manager: ```python theme={null} async with worker.acquire("user-42") as h: result = await h.turn("Hello") h.assist("The customer is a VIP - be especially warm.") print(result.text) ``` What `acquire` does: 1. Loads or creates the session for `session_id` 2. Acquires a per-session lock (serialises concurrent requests for the same id) 3. Yields a `SessionHandle` 4. On exit: persists state to the store and releases the lock Requests for **different** `session_id`s run fully in parallel. ## FastAPI multi-user example ```python theme={null} from contextlib import asynccontextmanager from fastapi import FastAPI from superdialog import DialogMachine, SessionWorker, InMemorySessionStore worker: SessionWorker @asynccontextmanager async def lifespan(app: FastAPI): global worker worker = SessionWorker( agent_factory=lambda: DialogMachine("kyc.yaml", llm="openai/gpt-4.1-mini"), store=InMemorySessionStore(), ) yield app = FastAPI(lifespan=lifespan) @app.post("/turn") async def turn(payload: dict): async with worker.acquire(payload["session_id"]) as h: result = await h.turn(payload["text"]) return {"reply": result.text} ``` ## Session stores | Store | Status | Use case | | ---------------------- | ------- | --------------------------------- | | `InMemorySessionStore` | ✅ v0.2 | Development, single-process | | `NullSessionStore` | ✅ v0.2 | Voice (ephemeral, no persistence) | | `RedisSessionStore` | 🔜 v0.3 | Multi-process, distributed | | `FileSessionStore` | 🔜 v0.3 | Lightweight persistence | | `SQLiteSessionStore` | 🔜 v0.3 | Local single-server | Switch stores by changing one line - the rest of the code stays the same: ```python theme={null} # Development store = InMemorySessionStore() # Production (v0.3) # store = RedisSessionStore(url="redis://localhost:6379") ``` ## Lock backends | Backend | Status | Use case | | -------------------- | ------- | --------------------------- | | `AsyncioLockBackend` | ✅ v0.2 | Single-process (default) | | `RedisLockBackend` | 🔜 v0.3 | Multi-process / distributed | ## Other agent brains When you want sessions and persistence but no checkpoint or flow opinion at all - a raw chat brain: ```python theme={null} from superdialog import LLMAgent, SessionWorker, InMemorySessionStore # Raw chat brain - no flow, no slots worker = SessionWorker( agent_factory=lambda: LLMAgent( llm="openai/gpt-5.1", system_prompt="You are a helpful customer support assistant.", ), store=InMemorySessionStore(), ) ``` Or with LangChain (requires `pip install superdialog[langchain]`): ```python theme={null} from superdialog import LangChainAgent, SessionWorker, InMemorySessionStore worker = SessionWorker( agent_factory=lambda: LangChainAgent(runnable=my_langchain_chain), store=InMemorySessionStore(), ) ``` ## Conversation state Internally, each session stores a `ChatContext` - LiveKit-aligned message history: ```python theme={null} @dataclass class ChatMessage: role: Literal["system", "user", "assistant", "tool"] content: str @dataclass class ChatContext: items: list[ChatMessage] ``` Beyond the transcript, each engine carries its own runtime state: * **Playbook engine** - the source of truth is the **event-sourced log** (`agent.event_log`); `ConversationState.fold(log, playbook)` derives the checkpoint, slots, and outcome. Persist `event_log.to_jsonl()` and restore via `load_event_log` for full fidelity. * **Legacy DialogMachine** - `FlowState` (current node, slot values). Present only when the brain is a `DialogMachine`; `None` otherwise. `SessionWorker`'s built-in `SessionRecord` persists `chat_ctx` / `flow_state` only. For durable or multi-worker resume on the Playbook engine, persist `agent.event_log.to_jsonl()` yourself and restore with `load_event_log` - otherwise playbook state fidelity (provisional vs confirmed slots, tool results) is lost. ## Voice (ephemeral) pattern For voice calls where each call is a fresh conversation and no persistence is needed: ```python theme={null} worker = SessionWorker( agent_factory=lambda: DialogMachine("kyc.yaml", llm="anthropic/claude-haiku-4-5"), store=NullSessionStore(), # writes are dropped; reads always return empty ) ``` `NullSessionStore` keeps the single-call lifecycle of a bare agent while still giving you the multiplexing and locking of `SessionWorker`. # Thinking in Playbooks Source: https://docs.unpod.ai/superdialog/thinking-in-playbooks The mental model shift from writing prompts to authoring checkpoints that gate outcomes - and why it makes complex conversations both fluid and reliable. # Thinking in Playbooks If you have built LLM chatbots before, SuperDialog's default engine asks for one shift in thinking: | Traditional bot | A rigid graph | SuperDialog playbook | | ---------------------- | ----------------------------------- | ------------------------------------------------------------- | | Write one long prompt | Wire every transition by hand | Author **checkpoints** that gate outcomes | | LLM decides everything | LLM can only traverse defined edges | LLM owns the phrasing; the framework owns the outcomes | | No structure | Users don't follow your graph | Conversation is free *inside* a checkpoint; progress is gated | The key move: **checkpoints gate outcomes, not utterances.** You don't script what the agent says next - you declare what "done" means for each step, and the model speaks freely to get there. ## The core model A **playbook** is one or more **journeys**, each a list of **checkpoints**. A checkpoint is a call-center-script unit with four parts: * **`goal`** - what "done" means for this step ("Have the city, date, and party size") * **`slots`** - typed data to extract while here (`city: str`, `date: date`, `players: int`) * **`guidance`** - prose the agent speaks from (it owns the wording) * **`advance_when`** - an ordered list of rules that move the conversation forward Those are the **full format** names. The **simple format** spells the same four `purpose` / `collect` / `say` / `done_when`; both compile to one checkpoint - see [Playbooks](/superdialog/playbooks). Animated Playbook checkpoint model diagram showing goal, slots, guidance, and advance rules inside a checkpoint, free conversation within it, and movement to the next checkpoint when an outcome is met. Inside a checkpoint the conversation is free - the caller can answer in any order, give everything in one breath, or change their mind. The framework's job is only to decide **when the goal is actually met** and where to go next. ## Start with the topology, then write the steps Before writing any prose, map your conversation: 1. What are the distinct **steps** (checkpoints) in this conversation? 2. What **data** (slots) must each step capture? 3. What **outcomes** move the conversation forward (advance rules)? 4. What can **go wrong** at each step? (caller refuses, asks a side question) Then write it in the **simple format** - prose steps and a persona, the same thing `superdialog generate` produces: ```yaml theme={null} goal: "Book a haircut and confirm it." persona: name: Mira voice_style: "Warm and brief. One question at a time." identity: "You are Mira, a booking assistant for Glow Studio." playbook: - id: greet purpose: "Open the call." say: "Greet the caller and ask how you can help." done_when: "Caller is ready to book." - id: collect purpose: "Get the booking details." say: "Ask for their name and preferred service." collect: [name, service] done_when: "Name and service are captured." - id: confirm purpose: "Confirm and close." say: "Read back the booking and confirm." done_when: "Caller has confirmed." ``` See [Playbooks](/superdialog/playbooks) for the full section reference and when to graduate to the full format (typed slots, gates, pipelines, multiple journeys). ## Test it - no infrastructure needed ```bash theme={null} superdialog generate "Book a haircut and confirm it." --output salon.yaml superdialog chat salon.yaml ``` Full interactive REPL against your playbook. No Unpod account, no phone number, no voice setup required. ``` > I'd like to book a haircut Hi! I'd be happy to help. May I have your name? > Mira, and I'd like a colour [checkpoint=collect ended=False] ``` The status line names the live checkpoint, so you can watch the conversation advance as outcomes are met. Iterate on `salon.yaml`, re-run `chat` - the loop takes seconds. ## Soft gates vs hard gates Every checkpoint has a `gate`. This is where fluidity meets reliability: * **Soft gate (default)** - provisional values are enough; the agent never blocks. The model keeps the conversation moving and the extracted data settles in the background. Use it for everything that isn't irreversible. * **Hard gate** - for payments, identity, anything you can't undo. Required slots must be **confirmed** (not just provisionally extracted), and the agent briefly waits for that confirmation before it speaks the gated line. A single model guess can never push past a hard gate on its own. ```yaml theme={null} playbook: - id: take_payment purpose: "Charge the deposit." say: "Confirm the amount, then take the card." collect: [card_token, amount] require: [card_token, amount] # must be confirmed, not just extracted gate: hard # waits for that before the gated line done_when: "Deposit charged." ``` ## Why one streaming call, not two A rigid graph has to make two LLM calls per turn: one to decide which edge fires, then one to speak. For voice, that adds latency before the caller hears anything. A playbook splits the turn instead: a fast **Talker** streams the spoken reply in one LLM call, while an async **Director** extracts slots and judges advance rules **off the speech path**. The caller hears the agent immediately; correctness converges a beat behind. Full runtime - ordering, the event log, barge-in safety - in [Architecture](/superdialog/architecture). ## When a graph still fits The checkpoint model is the default and the right choice for most conversations. A hand-authored **flow graph** still earns its place when: * **Compliance / auditability** - you need every reachable path enumerable and lintable as a spec. * **Strict determinism** - the conversation truly is a fixed decision tree with no room for the model to improvise. You don't lose anything by authoring a graph: by default it runs **compiled onto the Playbook engine** (`Playbook.load` detects flow JSON and converts it), and you can still run the original graph runtime with `engine="flow"` / `superdialog chat --mode flow`. See [Flows](/superdialog/flows) for graph authoring and the migration path. ## Next steps The simple and full authoring formats Generate and run your first playbook The Talker/Director runtime and event log Graph authoring and the migration path # Tools Source: https://docs.unpod.ai/superdialog/tools Give your agent the ability to call HTTP endpoints, Python functions, and MCP servers - declared in the playbook process layer and run by the Director off the speech path. ## Overview Tools let your agent take actions during a conversation - look up a record, hold a slot, charge a deposit, query a database. On the **Playbook engine** (the default), tools live in the playbook's **process layer** and are run by the **Director**, off the speech path, so a slow API call never stalls what the caller hears. Three tool shapes, one model: | Type | Execution | Best for | | -------- | ------------------------ | -------------------------------------------- | | `http` | HTTP request (templated) | External REST APIs, microservices | | `python` | In-process callable | Local functions, direct DB calls, any Python | | MCP | MCP protocol | Model Context Protocol servers | *** ## Declaring tools in a playbook A tool is a `ToolSpec` in the playbook's `tools:` list. HTTP tools template their `url`/`headers`/`body` with sandboxed Jinja over `{slots, env, results}`; the response is stored under `store_response_as` and is then readable as `results.` in guidance, advance rules, and other tools. ```yaml theme={null} tools: - id: hold_slot type: http method: POST url: "{{ env.API_BASE_URL }}/slots/hold" headers: {Authorization: "Bearer {{ env.ACCESS_TOKEN }}"} body: {city: "{{ slots.city }}", date: "{{ slots.date }}"} store_response_as: hold # readable as results.hold.* afterwards env_updates: {hold_id: hold_id} # env key <- dotted path into the response run_once: false # true: at most one call per session when: "slots.city" # expr over state; skip the call when falsy timeout: 10 ``` A checkpoint runs tools via its `pipeline` (on entry) or `on_enter` list. Tool failures are **data, not exceptions** - a failed HTTP status or template error is recorded as a failed result and routed declaratively, never a crash mid-call. ## Pipelines Chain tools with typed result branches. Each step routes on `ok`, `failed`, or an exact `http_`; failures can retry (capped) and route on exhaustion. ```yaml theme={null} pipelines: - id: confirm_and_hold steps: - tool: hold_slot on: ok: continue # next step, or pipeline success http_409: booking.offer_other # typed status branch failed: {retry: 2, on_exhaust: booking.collect} ``` A pipeline-owned checkpoint routes on `pipeline.ok` / `pipeline.failed`: ```yaml theme={null} - id: confirm gate: hard pipeline: confirm_and_hold advance_when: - {when: "pipeline.ok", judge: expr, to: booking.close} - {when: "pipeline.failed", judge: expr, to: booking.collect} ``` For auth that expires mid-call, one `middleware` entry refreshes a token and replays the step: ```yaml theme={null} middleware: {on_status: 401, refresh_with: refresh_auth, then: replay} ``` ## Python tools Declare a `python` tool in the playbook by id, then bind the implementation. A Python tool is an async callable that receives the call args and the current state: ```yaml theme={null} tools: - id: lookup_customer type: python args: {phone_number: {type: str, required: true}} store_response_as: customer ``` ```python theme={null} from superdialog.playbook import Playbook, PlaybookAgent, httpx_http async def lookup_customer(args, state) -> dict: """Look up a customer by phone number.""" return await crm.get_by_phone(args["phone_number"]) agent = PlaybookAgent( playbook=Playbook.load("booking.yaml"), talker_llm=talker, director_llm=director, http=httpx_http, python_tools={"lookup_customer": lookup_customer}, # bind by id ) ``` Through the unified `DialogMachine` entry point, pass any `Tool` and it is bridged automatically: ```python theme={null} from superdialog import DialogMachine, PythonTool agent = DialogMachine( "booking.yaml", llm="anthropic/claude-haiku-4-5", tools=[PythonTool.of(lookup_customer)], # bridged through the engine ) ``` *** ## MCP tools For servers that implement the [Model Context Protocol](https://modelcontextprotocol.io). Requires `pip install superdialog[mcp]`. ```python theme={null} from superdialog import MCPTool tool = MCPTool( id="search", name="search", description="Search the internal knowledge base", server="https://mcp.company.io", ) ``` `MCPTool` connects lazily on first use and forwards `execute(args)` to the configured server. Auto-discovery of all tools an MCP server publishes is planned for a follow-up release. *** ## Security model The playbook artifact is data and the transcript is untrusted user speech. Tools are defended accordingly: * **Sandboxed Jinja** for all `url`/`headers`/`body` rendering - attribute-walking injection payloads are blocked, not executed. * **Secret redaction** in the event log - token/api-key/password/bearer-shaped keys and URL userinfo are masked before the `ToolCallEvent` lands; the real request still goes to the wire untouched. * **The `env` lane is never rendered to the Talker** - `ACCESS_TOKEN`-class values cannot leak into speech or the packed prompt. *** ## Legacy: tools on the graph engine On the legacy DialogMachine graph engine (`engine="flow"`), tools are registered as a list and the LLM calls them via tool-calling. This still works for graph flows. ### `@tool` decorator and plain functions ```python theme={null} from superdialog.tools import tool @tool async def lookup_customer(customer_id: str) -> dict: """Look up customer record by ID.""" return await crm.get(customer_id) dm = DialogMachine(flow, llm="...", engine="flow", tools=[lookup_customer]) ``` Plain functions work too - SuperDialog wraps them using the function name as the id and the docstring as the description. ### `PythonTool` / `HttpTool` ```python theme={null} from superdialog import PythonTool, HttpTool import os tool = PythonTool.of(lookup_customer) # infer id/name/schema http = HttpTool( id="lookup", name="lookup", description="Look up a customer by partial Aadhaar", url="https://api.company.io/customer/lookup", auth={"type": "bearer", "token": os.environ["COMPANY_KEY"]}, ) ``` ### Function references on the flow model Attach functions to `ConversationFlow.tools` (flow-level) or `FlowNode.tools` (node-scoped) when building graphs in Python: ```python theme={null} from superdialog.flow.models import ConversationFlow, FlowNode, Edge from superdialog.machine.machine import DialogStateMachine from superdialog.tools import tool @tool async def search_kb(query: str) -> dict: """Search the knowledge base.""" return {"results": await kb.search(query)} async def check_availability(date: str) -> dict: """Check available appointment slots for a date.""" return {"slots": await calendar.get_slots(date)} flow = ConversationFlow( system_prompt="You are an appointment booking assistant.", initial_node="collect_info", tools=[search_kb], # available on every node nodes=[ FlowNode( id="collect_info", name="Collect Info", instruction="Collect patient name and preferred date.", tools=[check_availability], # node-scoped edges=[Edge(id="e_confirm", condition="All info collected", target_node_id="confirm")], ), ], ) machine = await DialogStateMachine.from_flow(flow, adapter) ``` ### Tool results and flow transitions On the graph engine, a tool can trigger a transition by returning a `ToolResult` with `transition_edge_id`: ```python theme={null} from superdialog.machine.models import ToolResult async def book_appointment(slot_id: str) -> ToolResult: """Book the appointment for the given slot.""" if await calendar.book(slot_id): return ToolResult(data={"booked": True}, transition_edge_id="booking_confirmed") return ToolResult(data={"booked": False}) ``` On the Playbook engine, outcome routing is done by **advance rules** and **pipeline branches** instead - a tool result is stored under `store_response_as` and read by `judge: expr` rules. There is no `transition_edge_id`. # Call Detail Records (CDR) Source: https://docs.unpod.ai/telephony/calls/call-logs GET /api/v2/platform/cdr/ Fetch telephony call detail records with filtering and pagination Fetch telephony call detail records (CDR) for your organization - inbound and outbound SIP calls with status, timing, duration, and end reason. **Prerequisites:** API Token + Org-Handle. See [Authentication](/api/get-started/authentication). ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | -------------------------- | | Authorization | string | Yes | `Token ` | | Org-Handle | string | Yes | Organization domain handle | ### Query parameters | Name | Type | Description | | ------------ | ------- | ---------------------------------------- | | page | integer | Page number (default `1`) | | page\_size | integer | Rows per page (default `20`) | | call\_type | string | `inbound` or `outbound` | | call\_status | string | `completed`, `notConnected`, or `failed` | ```json 200 theme={null} { "count": 643, "status_code": 200, "message": "Call logs fetched successfully", "data": [ { "id": 33364, "call_status": "completed", "end_reason": "call.in-progress.sip-completed-call", "call_type": "outbound", "bridge": { "id": 12, "name": "Acme Primary Bridge" }, "creation_time": "2025-11-08T05:32:29Z", "start_time": "2025-11-08T05:29:43Z", "end_time": "2025-11-08T05:32:28.686639Z", "call_duration": 165.686639, "source_number": "+15551234567", "destination_number": "+15559876543", "failure_source": null, "sip_cause": null } ] } ``` ```bash cURL theme={null} curl -s "https://unpod.ai/api/v2/platform/cdr/?page_size=5" \ -H "Authorization: Token " \ -H "Org-Handle: " ``` # Telephony Overview Source: https://docs.unpod.ai/telephony/calls/overview GET /api/v2/platform/telephony/overview/ Per-number lifecycle: connection state, termination, agent link, sync state A per-number lifecycle overview for your organization: one row per `ASSIGNED` number on your bridges, exposing `connection_state`, termination kind, agent link, and the projection `sync_state`. This endpoint is DB-only - it never live-probes the projection planes, so it stays fast and never errors on a missing projection. Secrets are masked. **Prerequisites:** API Token + Org-Handle. See [Authentication](/api/get-started/authentication). ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | -------------------------- | | Authorization | string | Yes | `Token ` | | Org-Handle | string | Yes | Organization domain handle | | Product-Id | string | No | Optional product filter | ### Row fields | Field | Type | Description | | ----------------- | ------- | -------------------------------------- | | number\_id | integer | Number id | | number | string | E.164 number | | bridge\_slug | string | Bridge the number is on | | connection\_state | string | e.g. `LINKED`, `NOT_LINKED` | | termination\_kind | string | `sip` (carrier) or `agent` | | agent\_id | string | Linked agent handle (null for carrier) | | sync\_state | string | Projection sync state | | sync\_detail | string | Extra sync detail (nullable) | ```json 200 theme={null} { "status_code": 200, "message": "Telephony overview fetched successfully.", "data": [ { "number_id": 501, "number": "+15551234567", "bridge_slug": "support-bridge", "connection_state": "LINKED", "termination_kind": "sip", "agent_id": null, "sync_state": "synced", "sync_detail": null } ] } ``` ```json 206 theme={null} { "message": "Please provide Org-Handle in headers" } ``` ```bash cURL theme={null} curl -s "https://unpod.ai/api/v2/platform/telephony/overview/" \ -H "Authorization: Token " \ -H "Org-Handle: " ``` # Daily (API) Source: https://docs.unpod.ai/telephony/integrations/daily/api Set up the Unpod side of a Daily integration over the REST API - create a SIP trunk to your Daily SIP dial-in room and attach your number. The **programmatic** path for the **Unpod side**: create a SIP trunk pointed at your **Daily SIP dial-in** address and attach your number. The trunk's **origin endpoint** is what Daily uses to reach Unpod - see the [Dashboard guide](/telephony/integrations/daily/dashboard) for the Daily-side setup. **Base URL:** `https://unpod.ai/api/v2/platform/` Every request needs `Authorization: Token ` and an `Org-Handle` header. See [Authentication](/api/get-started/authentication). ## Prerequisites * An Unpod **API Token** and **Org-Handle**. * A number in your org (we'll fetch its `id` below). * A Daily room with **SIP dial-in enabled** - note its `sip_uri` (`sip:@.sip.daily.co`). ## 1. Find your number ```bash cURL theme={null} curl https://unpod.ai/api/v2/platform/telephony/numbers/ \ -H "Authorization: Token $UNPOD_TOKEN" \ -H "Org-Handle: $ORG_HANDLE" ``` ```json Response (200) theme={null} { "status_code": 200, "message": "Telephony numbers fetched successfully.", "data": [ { "id": 501, "number": "+15551234567", "state": "NOT_ASSIGNED", "active": true } ] } ``` Full request/response schema. ## 2. Create the SIP trunk to Daily Point the trunk's `sip_url` at your Daily room's `sip_uri`. One trunk carries both inbound and outbound. ```bash cURL theme={null} curl -X POST https://unpod.ai/api/v2/platform/telephony/trunks/ \ -H "Authorization: Token $UNPOD_TOKEN" \ -H "Org-Handle: $ORG_HANDLE" \ -H "Content-Type: application/json" \ -d '{ "name": "Daily trunk", "sip_url": "sip:endpoint@your-subdomain.sip.daily.co", "transport": "udp", "port": "5060" }' ``` ```json Response (201) theme={null} { "status_code": 201, "message": "Trunk created successfully.", "data": { "id": 21, "name": "Daily trunk", "sip_url": "sip:endpoint@your-subdomain.sip.daily.co", "transport": "udp", "port": "5060", "active": true } } ``` All request fields (`sip_url`, `auth_username`, `auth_password`, `transport`, `port`, `source_ips`). ## 3. Attach your number to the trunk Map the number from step 1 (its `id`) to the trunk from step 2 (its `id`). The response returns the **origin endpoint** - the address + creds Daily uses to reach Unpod. ```bash cURL theme={null} curl -X POST https://unpod.ai/api/v2/platform/telephony/trunks/21/attach-numbers/ \ -H "Authorization: Token $UNPOD_TOKEN" \ -H "Org-Handle: $ORG_HANDLE" \ -H "Content-Type: application/json" \ -d '{ "number_ids": [501] }' ``` ```json Response (201) theme={null} { "status_code": 201, "message": "Numbers mapped to trunk.", "data": { "trunk_id": 21, "origin_endpoint": { "ingress": "sip:sip-lb1.unpod.tel", "dids": ["+15551234567"], "accepted_source_ips": [], "region": "ap-south" } } } ``` Path params, request body, and the full origin-endpoint response. ## Configure the Daily side Enable **SIP dial-in** on your Daily room. This is **Daily's own API** (`api.daily.co`, `Authorization: Bearer` with your Daily key). Run this **first** - the `sip_uri` it returns is exactly what you put in the trunk's `sip_url` in step 2. ### 4. Enable SIP dial-in on the room ```bash cURL theme={null} curl -X POST https://api.daily.co/v1/rooms/your-room-name \ -H "Authorization: Bearer $DAILY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "properties": { "sip": { "sip_mode": "dial-in", "display_name": "SIP Participant", "num_endpoints": 1, "codecs": { "audio": ["OPUS"] } } } }' ``` ```json Response theme={null} { "config": { "sip_uri": { "endpoint": "sip:123456780@example.sip.daily.co" }, "sip": { "sip_mode": "dial-in", "display_name": "SIP Participant" } } } ``` `config.sip_uri.endpoint` is the address you point the Unpod trunk at (the `sip_url` in step 2). Route that room to your agent so calls from Unpod connect to it. Full room `sip` properties + `sip_uri` response (this is Daily's API). ## Troubleshooting | Status | Meaning | Fix | | ------------------- | ----------------- | --------------------------------------- | | `400` on `/trunks/` | Missing `sip_url` | Send your Daily room `sip_uri` | | `400` on attach | Number not in org | Use a valid `id` from **GET /numbers/** | | `401` | Bad Unpod token | Verify `Authorization: Token …` | | `403` | Wrong org | Verify the `Org-Handle` header | The same flow in the Studio UI. Back to the integrations overview. # Daily (Dashboard) Source: https://docs.unpod.ai/telephony/integrations/daily/dashboard Connect Daily to Unpod with no code - create a SIP trunk on your Unpod number and point it at your Daily SIP dial-in room. The **no-code** path. Create a SIP trunk on your Unpod number, point it at your **Daily SIP dial-in** address, then copy the trunk's origin-endpoint credentials so Daily can reach Unpod. One trunk carries inbound and outbound. Prefer code? See the [API guide](/telephony/integrations/daily/api). **You need:** an Unpod number on a Bridge, and a Daily account with **SIP dial-in enabled** on a room (this gives you a `sip:...@.sip.daily.co` address). We only show the **Unpod-side** screens here. Treat your SIP trunk username/password as secrets - never paste them into shared docs or screenshots. ## Part 1 - Create the trunk in Unpod In **Telephony**, select your number and click **Configure**. On the **New trunk** tab, set a **Name** you'll recognise and the **trunk origin endpoint**: * **SIP URI / address** - your **Daily SIP dial-in** address (e.g. `sip:endpoint@your-subdomain.sip.daily.co`). * **Port** `5060` and **Transport** (`TCP` / `UDP` / `TLS`). * **Allowed IPs / CIDR** - optional source allow-list. Click **Test** to validate, then **Create Trunk**. Unpod - configure a new trunk pointing at Daily The number now shows **Linked · Daily**. Under **Origin Endpoint Details** copy the values - Daily uses them to reach Unpod: * **Address** - e.g. `sip-lb1.unpod.tel` * **Port** / **Protocol** - `5060` / `UDP` * **Username** and **Password** (under **Authentication**) Unpod - linked trunk origin endpoint credentials ## Part 2 - Enable SIP dial-in on Daily Daily has **no fixed SIP host** - each room exposes its own SIP address. Enable SIP dial-in so Daily gives you the `sip_uri` you entered in Part 1. Enable SIP dial-in on the Daily room (set the room's `sip` property with `sip_mode: "dial-in"`). Daily returns a read-only **`sip_uri`** in the form `sip:@.sip.daily.co`. This is **API-only** - Daily has no dashboard screen for it. Point that room / SIP dial-in at the agent that should answer, so incoming calls from the Unpod trunk connect to it. Daily's SIP dial-in setup is **API-driven** (room `sip` property / `pinless_dialin`). See [Daily's SIP dial-in docs](https://docs.daily.co/guides/products/dial-in-dial-out/sip). ## Part 3 - Publish Back in the Unpod Studio, click **Publish** to activate the configuration. The number is then ready for inbound and outbound calls. ## Troubleshooting | Symptom | Likely cause | Fix | | ----------------------------- | ------------------------------------- | -------------------------------------------------------------------------- | | Trunk **Test** fails in Unpod | Wrong Daily `sip_uri` or transport | Re-copy the room's `sip_uri`; match transport | | Daily can't reach the trunk | Wrong origin-endpoint address or auth | Re-copy **Address** + **Username/Password** from the Unpod origin endpoint | | Call connects, agent silent | No agent bound to the room | Route the SIP dial-in room to an agent | | Inbound not arriving | SIP dial-in not enabled | Set `sip_mode: "dial-in"` on the Daily room | | Number not reachable | Config not published | Click **Publish** in the Unpod Studio | Same flow over the REST API. Back to the integrations overview. # ElevenLabs (API) Source: https://docs.unpod.ai/telephony/integrations/elevenlabs/api Set up the Unpod side of an ElevenLabs integration over the REST API - create a SIP trunk to ElevenLabs and attach your number. The **programmatic** path for the **Unpod side**: create a SIP trunk pointed at ElevenLabs and attach your number. The trunk's **origin endpoint** is what you then register in ElevenLabs as a BYO SIP trunk - see the [Dashboard guide](/telephony/integrations/elevenlabs/dashboard) for the ElevenLabs-side screens. **Base URL:** `https://unpod.ai/api/v2/platform/` Every request needs `Authorization: Token ` and an `Org-Handle` header. See [Authentication](/api/get-started/authentication). ## Prerequisites * An Unpod **API Token** and **Org-Handle**. * A number in your org (we'll fetch its `id` below). ## 1. Find your number ```bash cURL theme={null} curl https://unpod.ai/api/v2/platform/telephony/numbers/ \ -H "Authorization: Token $UNPOD_TOKEN" \ -H "Org-Handle: $ORG_HANDLE" ``` ```json Response (200) theme={null} { "status_code": 200, "message": "Telephony numbers fetched successfully.", "data": [ { "id": 501, "number": "+15551234567", "state": "NOT_ASSIGNED", "active": true } ] } ``` Full request/response schema. ## 2. Create the SIP trunk to ElevenLabs Point the trunk's `sip_url` at ElevenLabs' SIP host. One trunk carries both inbound and outbound. ```bash cURL theme={null} curl -X POST https://unpod.ai/api/v2/platform/telephony/trunks/ \ -H "Authorization: Token $UNPOD_TOKEN" \ -H "Org-Handle: $ORG_HANDLE" \ -H "Content-Type: application/json" \ -d '{ "name": "ElevenLabs trunk", "sip_url": "sip:sip.rtc.elevenlabs.io:5060;transport=tcp", "transport": "tcp", "port": "5060" }' ``` ```json Response (201) theme={null} { "status_code": 201, "message": "Trunk created successfully.", "data": { "id": 21, "name": "ElevenLabs trunk", "sip_url": "sip:sip.rtc.elevenlabs.io:5060;transport=tcp", "transport": "tcp", "port": "5060", "active": true } } ``` All request fields (`sip_url`, `auth_username`, `auth_password`, `transport`, `port`, `source_ips`). ## 3. Attach your number to the trunk Map the number from step 1 (its `id`) to the trunk from step 2 (its `id`). The response returns the **origin endpoint** - the address + creds you register in ElevenLabs. ```bash cURL theme={null} curl -X POST https://unpod.ai/api/v2/platform/telephony/trunks/21/attach-numbers/ \ -H "Authorization: Token $UNPOD_TOKEN" \ -H "Org-Handle: $ORG_HANDLE" \ -H "Content-Type: application/json" \ -d '{ "number_ids": [501] }' ``` ```json Response (201) theme={null} { "status_code": 201, "message": "Numbers mapped to trunk.", "data": { "trunk_id": 21, "origin_endpoint": { "ingress": "sip:sip-lb1.unpod.tel", "dids": ["+15551234567"], "accepted_source_ips": [], "region": "ap-south" } } } ``` The `origin_endpoint.ingress` (plus the trunk's `auth_username` / `auth_password`) is what you enter in ElevenLabs' BYO SIP trunk. Path params, request body, and the full origin-endpoint response. ## Configure the ElevenLabs side The Unpod side is done. Import the number into ElevenLabs over SIP. This is **ElevenLabs' own API** (`api.elevenlabs.io`, `xi-api-key` header). Use the origin endpoint from step 3. ### 4. Import the number from SIP trunk ```bash cURL theme={null} curl -X POST https://api.elevenlabs.io/v1/convai/phone-numbers \ -H "xi-api-key: $ELEVENLABS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "provider": "sip_trunk", "phone_number": "+15551234567", "label": "Unpod trunk", "outbound_trunk_config": { "address": "sip-lb1.unpod.tel", "transport": "tcp", "media_encryption": "allowed", "credentials": { "username": "", "password": "" } }, "inbound_trunk_config": { "allowed_addresses": ["0.0.0.0/0"] } }' ``` `outbound_trunk_config.address` = your `origin_endpoint.ingress` host (no `sip:`). The response returns a `phone_number_id` - attach your Conversational AI agent to it. Full phone-number import + agent assignment (this is ElevenLabs' API). ## Troubleshooting | Status | Meaning | Fix | | ------------------- | ----------------- | --------------------------------------------------- | | `400` on `/trunks/` | Missing `sip_url` | Send `sip:sip.rtc.elevenlabs.io:5060;transport=tcp` | | `400` on attach | Number not in org | Use a valid `id` from **GET /numbers/** | | `401` | Bad Unpod token | Verify `Authorization: Token …` | | `403` | Wrong org | Verify the `Org-Handle` header | # ElevenLabs (Dashboard) Source: https://docs.unpod.ai/telephony/integrations/elevenlabs/dashboard Connect ElevenLabs to Unpod with no code - connect a number in the Unpod Studio, then import it into ElevenLabs Conversational AI over SIP. The **no-code** path. Connect your number in the Unpod Studio to get an **Unpod SIP trunk** (one endpoint, inbound + outbound), then import that number into **ElevenLabs Conversational AI** over SIP and attach an agent. Prefer code? See the [API guide](/telephony/integrations/elevenlabs/api). **You need:** an Unpod number on a Bridge, and an ElevenLabs account with a **Conversational AI agent** (the agent needs a **First Message** and a selected voice). We only show the **Unpod-side** screens here. Treat your SIP trunk username/password as secrets - never paste them into shared docs or screenshots. ## Part 1 - Connect the number in Unpod In **Telephony**, select your number (shown as **Not Linked**) and click **Connect** to set up its SIP trunk. Unpod - number Not Linked, Connect In the trunk panel, set the **Trunk Origin Endpoint Details** to ElevenLabs' SIP host: * **SIP URI / address** - `sip:sip.rtc.elevenlabs.io:5060;transport=tcp` (or port `5061` with `transport=tls` for TLS). * **Port** `5060` (TCP) / `5061` (TLS). * **Allowed IPs / CIDR** - optional source allow-list. Click **Save**. The number then shows **Linked · Unpod SIP Trunk**. Calls Unpod sends to ElevenLabs use the form `sip:@sip.rtc.elevenlabs.io:5060` (identifier + domain). Under **Origin Endpoint Details** copy these - you'll paste them into ElevenLabs next: * **Address** - e.g. `sip-lb1.unpod.tel` * **Port** / **Protocol** - `5060` / `TCP` * **Username** and **Password** (under **Authentication**) Unpod - linked trunk origin endpoint credentials ## Part 2 - Import the number into ElevenLabs In the **ElevenLabs Agents** dashboard, open the **Phone Numbers** section. Click **Import number**, then choose **Import a phone number from SIP trunk**. ElevenLabs - Phone Numbers, Import number In the **Import SIP Trunk** panel set: * **Label** - a descriptive name. * **Phone Number** - your Unpod number in E.164 (e.g. `+918071539111`), or a SIP extension / identifier. ElevenLabs - Import SIP Trunk panel Under **Inbound Configuration** (forwards calls to the ElevenLabs SIP server): * **Media Encryption** - `Allowed` (use `Required` with TLS for production). * **Allowed Numbers** *(optional)* - leave empty to allow all. * **Allowed Source IP Addresses** *(optional)* - `0.0.0.0/0` to allow all (TCP/TLS only; restrict in production). So ElevenLabs can send calls to Unpod, enter the Unpod origin-endpoint values from Part 1: * **Address** - your Unpod address, **hostname only, no `sip:` prefix** (e.g. `sip-lb1.unpod.tel`). * **Transport Type** - `TCP` (or `TLS`). * **Media Encryption** - `Allowed` (use `Required` with TLS for production). * **SIP Trunk Username** / **SIP Trunk Password** - the Unpod **Username** / **Password**. Click **Import**. **Production:** ElevenLabs recommends **TLS transport + Required media encryption** (TLS 1.2+). Your system must support **G711 or G722** codecs (8 kHz / 16 kHz) or resample. ## Part 3 - Attach your agent Open the imported number and **attach** your Conversational AI **agent** from the dropdown. Incoming calls now route through ElevenLabs to that agent. From **Phone Numbers**, select the imported number → **Make Outbound Call** → choose the agent → enter the destination in E.164. Check the agent's **Call History** / **Conversations** for transcripts and recordings. ## Troubleshooting | Symptom | Likely cause | Fix | | --------------------- | ------------------------------------ | --------------------------------------------------------------------------------- | | SIP `408` timeout | Wrong trunk address or transport | Confirm the Unpod address and **TCP** transport on both sides | | Authentication failed | Wrong / mis-cased credentials | Re-copy the **Username/Password** from the Unpod origin endpoint (case-sensitive) | | Call connects, silent | Agent missing First Message or voice | Set a **First Message** and a valid voice on the agent | | Inbound not arriving | Unpod trunk SIP URI wrong | Set it to `sip:sip.rtc.elevenlabs.io:5060` (TCP) | | Number not reachable | Config not published | Click **Publish** in the Unpod Studio | Same flow over the REST API. Back to the integrations overview. # LiveKit (API) Source: https://docs.unpod.ai/telephony/integrations/livekit/api Set up the Unpod side of a LiveKit integration over the REST API - create a SIP trunk to LiveKit and attach your number. The **programmatic** path for the **Unpod side**: create a SIP trunk pointed at your LiveKit SIP URI and attach your number. The trunk's **origin endpoint** is what you then use to create the matching trunks in the LiveKit console - see the [Dashboard guide](/telephony/integrations/livekit/dashboard) for the LiveKit-side screens. **Base URL:** `https://unpod.ai/api/v2/platform/` Every request needs `Authorization: Token ` and an `Org-Handle` header. See [Authentication](/api/get-started/authentication). ## Prerequisites * An Unpod **API Token** and **Org-Handle**. * A number in your org (we'll fetch its `id` below). * Your **LiveKit SIP URI** from the LiveKit Cloud dashboard (**Settings → Project**), e.g. `sip:.sip.livekit.cloud`. ## 1. Find your number ```bash cURL theme={null} curl https://unpod.ai/api/v2/platform/telephony/numbers/ \ -H "Authorization: Token $UNPOD_TOKEN" \ -H "Org-Handle: $ORG_HANDLE" ``` ```json Response (200) theme={null} { "status_code": 200, "message": "Telephony numbers fetched successfully.", "data": [ { "id": 501, "number": "+15551234567", "state": "NOT_ASSIGNED", "active": true } ] } ``` Full request/response schema. ## 2. Create the SIP trunk to LiveKit Point the trunk's `sip_url` at your LiveKit SIP URI. One trunk carries both inbound and outbound. ```bash cURL theme={null} curl -X POST https://unpod.ai/api/v2/platform/telephony/trunks/ \ -H "Authorization: Token $UNPOD_TOKEN" \ -H "Org-Handle: $ORG_HANDLE" \ -H "Content-Type: application/json" \ -d '{ "name": "LiveKit trunk", "sip_url": "sip:.sip.livekit.cloud", "transport": "tcp", "port": "5060" }' ``` ```json Response (201) theme={null} { "status_code": 201, "message": "Trunk created successfully.", "data": { "id": 21, "name": "LiveKit trunk", "sip_url": "sip:.sip.livekit.cloud", "transport": "tcp", "port": "5060", "active": true } } ``` All request fields (`sip_url`, `auth_username`, `auth_password`, `transport`, `port`, `source_ips`). ## 3. Attach your number to the trunk Map the number from step 1 (its `id`) to the trunk from step 2 (its `id`). The response returns the **origin endpoint** - the address + creds you use in LiveKit. ```bash cURL theme={null} curl -X POST https://unpod.ai/api/v2/platform/telephony/trunks/21/attach-numbers/ \ -H "Authorization: Token $UNPOD_TOKEN" \ -H "Org-Handle: $ORG_HANDLE" \ -H "Content-Type: application/json" \ -d '{ "number_ids": [501] }' ``` ```json Response (201) theme={null} { "status_code": 201, "message": "Numbers mapped to trunk.", "data": { "trunk_id": 21, "origin_endpoint": { "ingress": "sip:sip-lb1.unpod.tel", "dids": ["+15551234567"], "accepted_source_ips": [], "region": "ap-south" } } } ``` The `origin_endpoint.ingress` (plus the trunk's `auth_username` / `auth_password`) is what you enter when creating the LiveKit outbound trunk. Path params, request body, and the full origin-endpoint response. ## Configure the LiveKit side The Unpod side is done. LiveKit's SIP API is served over **Twirp HTTP** at `{LIVEKIT_URL}/twirp/livekit.SIP/`. Authenticate with a **LiveKit access token** (JWT with a SIP admin grant) - mint one with `lk token create --sip-admin` or a server SDK. ```bash Env theme={null} export LIVEKIT_URL="https://.livekit.cloud" export LIVEKIT_TOKEN="" ``` ### 4. Create the inbound trunk ```bash cURL theme={null} curl -X POST "$LIVEKIT_URL/twirp/livekit.SIP/CreateSIPInboundTrunk" \ -H "Authorization: Bearer $LIVEKIT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "trunk": { "name": "Unpod inbound", "numbers": ["+15551234567"] } }' ``` ```json Response theme={null} { "sip_trunk_id": "ST_inbound123", "name": "Unpod inbound", "numbers": ["+15551234567"] } ``` ### 5. Create the outbound trunk ```bash cURL theme={null} curl -X POST "$LIVEKIT_URL/twirp/livekit.SIP/CreateSIPOutboundTrunk" \ -H "Authorization: Bearer $LIVEKIT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "trunk": { "name": "Unpod outbound", "address": "sip-lb1.unpod.tel", "destination_country": "US", "transport": "SIP_TRANSPORT_TCP", "numbers": ["+15551234567"], "auth_username": "", "auth_password": "" } }' ``` ```json Response theme={null} { "sip_trunk_id": "ST_outbound456", "name": "Unpod outbound", "address": "sip-lb1.unpod.tel" } ``` `trunk.address` = your `origin_endpoint.ingress` host (no `sip:` prefix). ### 6. Create the dispatch rule ```bash cURL theme={null} curl -X POST "$LIVEKIT_URL/twirp/livekit.SIP/CreateSIPDispatchRule" \ -H "Authorization: Bearer $LIVEKIT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "dispatch_rule": { "rule": { "dispatchRuleIndividual": { "roomPrefix": "call-" } } }, "trunk_ids": ["ST_inbound123"] }' ``` ```json Response theme={null} { "sip_dispatch_rule_id": "SDR_abc789", "trunk_ids": ["ST_inbound123"] } ``` The same calls are available via the **`lk` CLI** (`lk sip inbound create `) and the server SDKs (Go `SIPClient`, JS `SipClient`, Python `sip_service`). Field shapes follow LiveKit's SIP proto - confirm in their API reference. Full trunk + dispatch-rule API and SDK usage (this is LiveKit's API). ## Troubleshooting | Status | Meaning | Fix | | ------------------- | ----------------- | ---------------------------------------------- | | `400` on `/trunks/` | Missing `sip_url` | Send your `sip:.sip.livekit.cloud` | | `400` on attach | Number not in org | Use a valid `id` from **GET /numbers/** | | `401` | Bad Unpod token | Verify `Authorization: Token …` | | `403` | Wrong org | Verify the `Org-Handle` header | # LiveKit (Dashboard) Source: https://docs.unpod.ai/telephony/integrations/livekit/dashboard Connect LiveKit to Unpod with no code - connect a number in the Unpod Studio, then point a LiveKit SIP trunk at it for low-latency streaming. The **no-code** path. Connect your number in the Unpod Studio to get an **Unpod SIP trunk** (one endpoint, inbound + outbound), then create matching trunks in the **LiveKit Cloud** console using those credentials. Prefer code? See the [API guide](/telephony/integrations/livekit/api). **You need:** an Unpod number on a Bridge, a **LiveKit Cloud** project, and a **LiveKit agent** to answer inbound calls. The Unpod trunk carries both inbound and outbound - no separate provider setup. Keep your LiveKit API Secret and SIP trunk credentials private - never paste them into shared docs or screenshots. ## Part 1 - Get the LiveKit SIP URI In the **LiveKit Cloud** dashboard, open **Settings → Project** (General) and copy the **SIP URI** - it looks like `sip:.sip.livekit.cloud`. You paste this into the Unpod trunk next so outbound calls reach LiveKit. ## Part 2 - Connect the number in Unpod In **Telephony**, select your number (shown as **Not Linked**) and click **Connect** to set up its SIP trunk. Unpod - number Not Linked, Connect In the trunk panel, set the **Trunk Origin Endpoint Details**: * **SIP URI / address** - paste the **LiveKit SIP URI** from Part 1 (e.g. `sip:.sip.livekit.cloud`). * **Port** `5060`, **Transport** `TCP` (or `UDP` / `TLS` to match LiveKit). * **Allowed IPs / CIDR** - optional source allow-list. Click **Save**. The number then shows **Linked · Unpod SIP Trunk**. Unpod - trunk SIP URI pointing at LiveKit Under **Origin Endpoint Details** copy these - you'll use them in LiveKit next: * **Address** - e.g. `sip-lb1.unpod.tel` * **Port** / **Protocol** - `5060` / `UDP` * **Username** and **Password** (under **Authentication**) Unpod - linked trunk origin endpoint credentials ## Part 3 - Create the trunks in LiveKit In the LiveKit console, go to **Telephony → SIP trunks** and click **Create new trunk**. Fill the **Trunk details** form (or use the **JSON editor**). Create two trunks using the Unpod values from Part 2. Set **Trunk direction** to **Inbound**, then: * **Trunk name** - a label. * **Numbers** - your Unpod phone number(s) in E.164 (comma-separated). Leave empty to accept any. * **Allowed addresses** - restrict to trusted IPs, or `0.0.0.0/0` to allow all (while testing). LiveKit - create inbound SIP trunk Set **Trunk direction** to **Outbound**, then map the Unpod values: * **Trunk name** - a label. * **Address** - Unpod **Address** (e.g. `sip-lb1.unpod.tel`). * **Transport** - `TCP` (match your trunk). * **Numbers** - your Unpod phone number(s). Under **Optional settings**, add the **Auth username / password** from the Unpod origin endpoint. LiveKit - create outbound SIP trunk ## Part 4 - Route inbound to your agent In **Telephony → Dispatch rules**, click **Create new dispatch rule** so inbound calls land in a room your agent joins: * **Rule name** - a label. * **Rule type** - **Individual**. * **Room prefix** - e.g. `call-`. * **Agent dispatch** - click **Add agent** and enter your deployed agent name (for explicit dispatch). * **Inbound routing** - match by **Phone numbers** or **Trunks** (leave unset to apply to all). LiveKit - create dispatch rule A single Individual dispatch rule is usually the only routing config you need. ## Publish Back in the Unpod Studio, click **Publish** to activate the configuration. The number is then ready for inbound and outbound calls. ## Troubleshooting | Symptom | Likely cause | Fix | | --------------------------- | ----------------------------------- | -------------------------------------------------------------------------- | | LiveKit can't place calls | Wrong outbound address or auth | Re-copy **Address** + **Username/Password** from the Unpod origin endpoint | | Inbound not arriving | Number not on the inbound trunk | Confirm the Unpod number is in the inbound trunk's `numbers` (E.164) | | Call connects, agent silent | Dispatch rule / agent name mismatch | Make **Agent Name** match the deployed agent exactly | | Number not reachable | Config not published | Click **Publish** in the Unpod Studio | Same flow over the REST API. Back to the integrations overview. # Integrations & SDKs Source: https://docs.unpod.ai/telephony/integrations/overview Connect Unpod to every major AI voice platform - Vapi, LiveKit, Twilio, Daily and more - over global SIP trunking and a single REST API. Unpod is built ground-up for **AI-first interconnectivity**. Route telephone audio straight into any major voice platform using **SIP trunking** or **WebSocket streaming** - no custom bridges required. Every provider plugs into the same trunk → number → agent model, configurable from the **Console** or the **REST API**. Two ways to connect any platform: the **Dashboard** (no-code, click-through in the Unpod Studio) or the **API** (programmatic, language-agnostic REST). Pick either - they drive the same underlying trunk. ## Supported Platforms
Voice platforms
## Official SDKs
Build in your language
Python is the only first-party SDK today. Other languages integrate through the [REST API](/api/overview) or an [HTTP brain](/speech-stack/adapters#bundled-adapters) until native SDKs ship. ## Prerequisites Sign up and grab an **API Token** + **Org-Handle**. See [Authentication](/api/get-started/authentication). Bring a number into your org so calls have a DID. See the [Quickstart](/telephony/quickstart). A trunk holds your carrier credentials and exposes the origin endpoint. See [Create a trunk](/telephony/trunks/create-trunk). Pick a platform above and run its **Dashboard** or **API** integration guide. ## Getting Started | If you want to… | Recommended platform | | ------------------------------------------- | -------------------- | | Ship production AI voice agents fast | **Vapi** | | Run the lowest-latency phone pipeline | **LiveKit** | | Build multi-party voice + video | **Daily** | | Drive calls programmatically from any stack | **REST API** | The fastest path - Dashboard or API, your choice. How trunks, numbers, and agents fit together end to end. # Ultravox (API) Source: https://docs.unpod.ai/telephony/integrations/ultravox/api Place outbound Ultravox AI calls through your Unpod number over SIP - create the Unpod trunk, attach your number, then trigger the call from Ultravox. The **programmatic** path: on the **Unpod side** create a SIP trunk and attach your number to get a SIP endpoint + credentials; then call **Ultravox's Create Agent Call** endpoint with those SIP details. Ultravox places the outbound call through your Unpod trunk. Prefer clicks? See the [Dashboard guide](/telephony/integrations/ultravox/dashboard). **Unpod Base URL:** `https://unpod.ai/api/v2/platform/` Every Unpod request needs `Authorization: Token ` and an `Org-Handle` header. See [Authentication](/api/get-started/authentication). ## Prerequisites * An Unpod **API Token** and **Org-Handle**, plus a number on a Bridge. * An Ultravox **API key** and an **agent** (note its **Agent ID**). ## 1. Find your Unpod number ```bash cURL theme={null} curl https://unpod.ai/api/v2/platform/telephony/numbers/ \ -H "Authorization: Token $UNPOD_TOKEN" \ -H "Org-Handle: $ORG_HANDLE" ``` ```json Response (200) theme={null} { "status_code": 200, "message": "Telephony numbers fetched successfully.", "data": [ { "id": 501, "number": "+918071539111", "state": "ASSIGNED", "active": true } ] } ``` Full request/response schema. ## 2. Create the SIP trunk Set `auth_username` / `auth_password` - these become the SIP credentials Ultravox authenticates with when it dials into Unpod. ```bash cURL theme={null} curl -X POST https://unpod.ai/api/v2/platform/telephony/trunks/ \ -H "Authorization: Token $UNPOD_TOKEN" \ -H "Org-Handle: $ORG_HANDLE" \ -H "Content-Type: application/json" \ -d '{ "name": "Ultravox trunk", "sip_url": "sip:sip-lb1.unpod.tel", "auth_username": "unpod_user_501", "auth_password": "", "transport": "udp", "port": "5060" }' ``` ```json Response (201) theme={null} { "status_code": 201, "message": "Trunk created successfully.", "data": { "id": 21, "name": "Ultravox trunk", "auth_username": "unpod_user_501", "active": true } } ``` All request fields (`sip_url`, `auth_username`, `auth_password`, `transport`, `port`, `source_ips`). ## 3. Attach your number to the trunk Map the number from step 1 (its `id`) to the trunk from step 2 (its `id`). The response returns the **origin endpoint** - the SIP domain Ultravox dials into. ```bash cURL theme={null} curl -X POST https://unpod.ai/api/v2/platform/telephony/trunks/21/attach-numbers/ \ -H "Authorization: Token $UNPOD_TOKEN" \ -H "Org-Handle: $ORG_HANDLE" \ -H "Content-Type: application/json" \ -d '{ "number_ids": [501] }' ``` ```json Response (201) theme={null} { "status_code": 201, "message": "Numbers mapped to trunk.", "data": { "trunk_id": 21, "origin_endpoint": { "ingress": "sip:sip-lb1.unpod.tel", "dids": ["+918071539111"] } } } ``` You now have everything Ultravox needs: * **SIP domain** - host from `origin_endpoint.ingress` (e.g. `sip-lb1.unpod.tel`, drop the `sip:`). * **`auth_username`** / **`auth_password`** - the creds you set in step 2. * Your **number** (`+918071539111`) for caller ID. Path params, request body, and the full origin-endpoint response. ## 4. Place the call from Ultravox This is **Ultravox's own API** (`api.ultravox.ai`, `X-API-Key` header). Point `to` at your Unpod SIP domain and pass the Unpod credentials. ```bash cURL theme={null} curl -X POST https://api.ultravox.ai/api/agents/YOUR_AGENT_ID/calls \ -H "X-API-Key: YOUR_ULTRAVOX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "medium": { "sip": { "outgoing": { "to": "sip:+919999999999@sip-lb1.unpod.tel", "from": "+918071539111", "username": "unpod_user_501", "password": "" } } }, "firstSpeakerSettings": { "user": {} } }' ``` | Field | Value | | ----------------------- | ----------------------------------------------------- | | `to` | Destination as `sip:@` | | `from` | Your Unpod number (caller ID) | | `username` / `password` | Unpod `auth_username` / `auth_password` from step 2 | | `firstSpeakerSettings` | `{ "user": {} }` - callee speaks first (outbound) | Ultravox returns a `callId` and sends the SIP INVITE through Unpod. The agent joins when the callee answers. Full request body + response (this is Ultravox's API). ## 5. Debug with SIP logs If a call is rejected, pull Ultravox's SIP logs for that `callId`: ```bash cURL theme={null} curl https://api.ultravox.ai/api/calls/CALL_ID/sip/logs \ -H "X-API-Key: YOUR_ULTRAVOX_API_KEY" ``` SIP log fields for debugging rejected calls (this is Ultravox's API). ## Troubleshooting | Status | Meaning | Fix | | ------------- | ----------------------- | ---------------------------------------------------------------- | | `403` | Auth rejected by Unpod | Re-check `username` / `password` (Unpod trunk creds from step 2) | | `407` | Proxy auth required | Ensure `username` / `password` are in `sip.outgoing` | | `480` | Destination unavailable | Verify the `to` number; confirm your Unpod number is active | | `401` (Unpod) | Bad Unpod token | Verify `Authorization: Token …` on Unpod calls | The same flow, gathering creds in the UIs. Back to the integrations overview. # Ultravox (Dashboard) Source: https://docs.unpod.ai/telephony/integrations/ultravox/dashboard Connect Ultravox to Unpod - use your Unpod number as the SIP trunk that Ultravox places outbound AI voice calls through. Use your **Unpod number as the SIP trunk** for **Ultravox** outbound calls. You gather credentials from both dashboards, then Ultravox places the call through Unpod over SIP - reaching regular phone numbers worldwide. Prefer raw requests? See the [API guide](/telephony/integrations/ultravox/api). **You need:** an Unpod number on a Bridge, and an Ultravox account with an **agent** and an **API key**. Treat your SIP username/password and API key as secrets - never paste them into shared docs or screenshots. ## What you'll collect | Service | Item | Where | | -------- | ------------------------ | -------------------------------- | | Ultravox | **API key** | Ultravox dashboard | | Ultravox | **Agent ID** | Ultravox dashboard | | Unpod | **SIP domain / address** | Number's trunk → Origin Endpoint | | Unpod | **Username** | Number's trunk → Origin Endpoint | | Unpod | **Password** | Number's trunk → Origin Endpoint | | Unpod | **Phone number** | Telephony → Numbers | ## Part 1 - Get your Ultravox agent + API key In the Ultravox console, open **Agents** and create an agent (set its system prompt and voice). Copy its **Agent ID** - you'll pass it in the call request. Ultravox - Agents page, create a new agent Generate an **API key** in the Ultravox console and copy it. Ultravox - API key ## Part 2 - Get your Unpod SIP credentials In **Telephony**, select your number (shown as **Not Linked**) and click **Connect** to set up its SIP trunk. Unpod - number Not Linked, Connect Open the trunk panel and under **Origin Endpoint Details** copy: * **Address** - your Unpod SIP domain (e.g. `sip-lb1.unpod.tel`). * **Username** and **Password** (under **Authentication**). * Your **phone number** in E.164 (e.g. `+918071539111`). Unpod - trunk origin endpoint credentials ## Part 3 - Place the call through Ultravox Ultravox triggers the outbound call over your Unpod trunk. There's no dashboard button for this - send one request to Ultravox's **Create Agent Call** endpoint with the SIP details from Parts 1-2: ```bash cURL theme={null} curl -X POST https://api.ultravox.ai/api/agents/YOUR_AGENT_ID/calls \ -H "X-API-Key: YOUR_ULTRAVOX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "medium": { "sip": { "outgoing": { "to": "sip:+919999999999@", "from": "+918071539111", "username": "", "password": "" } } }, "firstSpeakerSettings": { "user": {} } }' ``` * **`to`** - destination number as a SIP URI at your Unpod domain. * **`from`** - your Unpod number (caller ID). * **`username` / `password`** - the Unpod origin-endpoint credentials. The agent answers when the callee picks up. See the [API guide](/telephony/integrations/ultravox/api) for the full request/response and debugging. ## Troubleshooting | Symptom | Likely cause | Fix | | ---------------- | ----------------------- | ----------------------------------------------------------- | | SIP `403` | Auth rejected | Re-copy the Unpod **Username/Password** (case-sensitive) | | SIP `407` | Proxy auth required | Confirm `username`/`password` are sent in `sip.outgoing` | | SIP `480` | Destination unavailable | Verify the `to` number and that your Unpod number is active | | Call never rings | Wrong SIP domain | Use the exact **Address** from the Unpod origin endpoint | Full request/response + SIP log debugging. Back to the integrations overview. # Vapi (API) Source: https://docs.unpod.ai/telephony/integrations/vapi/api Set up the Unpod side of a Vapi integration over the REST API - create a SIP trunk to Vapi and attach your number. The **programmatic** path for the **Unpod side**: create a SIP trunk pointed at Vapi and attach your number. The trunk's **origin endpoint** is what you then register in Vapi as a BYO SIP trunk - see the [Dashboard guide](/telephony/integrations/vapi/dashboard) for the Vapi-side screens. **Base URL:** `https://unpod.ai/api/v2/platform/` Every request needs `Authorization: Token ` and an `Org-Handle` header. See [Authentication](/api/get-started/authentication). ## Prerequisites * An Unpod **API Token** and **Org-Handle**. * A number in your org (we'll fetch its `id` below). ## 1. Find your number List your org's numbers and note the `id` of the one you want to route. ```bash cURL theme={null} curl https://unpod.ai/api/v2/platform/telephony/numbers/ \ -H "Authorization: Token $UNPOD_TOKEN" \ -H "Org-Handle: $ORG_HANDLE" ``` ```json Response (200) theme={null} { "status_code": 200, "message": "Telephony numbers fetched successfully.", "data": [ { "id": 501, "number": "+15551234567", "state": "NOT_ASSIGNED", "active": true } ] } ``` Full request/response schema. ## 2. Create the SIP trunk to Vapi Point the trunk's `sip_url` at Vapi's SIP host. One trunk carries both inbound and outbound. ```bash cURL theme={null} curl -X POST https://unpod.ai/api/v2/platform/telephony/trunks/ \ -H "Authorization: Token $UNPOD_TOKEN" \ -H "Org-Handle: $ORG_HANDLE" \ -H "Content-Type: application/json" \ -d '{ "name": "Vapi trunk", "sip_url": "sip:sip.vapi.ai", "transport": "tcp", "port": "5060" }' ``` ```json Response (201) theme={null} { "status_code": 201, "message": "Trunk created successfully.", "data": { "id": 21, "name": "Vapi trunk", "sip_url": "sip:sip.vapi.ai", "transport": "tcp", "port": "5060", "active": true } } ``` All request fields (`sip_url`, `auth_username`, `auth_password`, `transport`, `port`, `source_ips`). ## 3. Attach your number to the trunk Map the number from step 1 (its `id`) to the trunk from step 2 (its `id`). The response returns the **origin endpoint** - the address + creds you register in Vapi. ```bash cURL theme={null} curl -X POST https://unpod.ai/api/v2/platform/telephony/trunks/21/attach-numbers/ \ -H "Authorization: Token $UNPOD_TOKEN" \ -H "Org-Handle: $ORG_HANDLE" \ -H "Content-Type: application/json" \ -d '{ "number_ids": [501] }' ``` ```json Response (201) theme={null} { "status_code": 201, "message": "Numbers mapped to trunk.", "data": { "trunk_id": 21, "origin_endpoint": { "ingress": "sip:sip-lb1.unpod.tel", "dids": ["+15551234567"], "accepted_source_ips": [], "region": "ap-south" } } } ``` The `origin_endpoint.ingress` (plus the trunk's `auth_username` / `auth_password`) is what you enter in Vapi's BYO SIP trunk. Path params, request body, and the full origin-endpoint response. ## Configure the Vapi side The Unpod side is done. Register the origin endpoint in Vapi as a **BYO SIP trunk**, then import the number. These are **Vapi's own APIs** (`api.vapi.ai`, `Authorization: Bearer `). ### 4. Create the BYO SIP trunk credential ```bash cURL theme={null} curl -X POST https://api.vapi.ai/credential \ -H "Authorization: Bearer $VAPI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "provider": "byo-sip-trunk", "name": "Unpod trunk", "gateways": [{ "ip": "sip-lb1.unpod.tel", "inboundEnabled": true }], "outboundAuthenticationPlan": { "authUsername": "", "authPassword": "" } }' ``` `gateways[].ip` = your Unpod `origin_endpoint.ingress` (host only, no `sip:`). Copy the returned credential `id`. ### 5. Import the number ```bash cURL theme={null} curl -X POST https://api.vapi.ai/phone-number \ -H "Authorization: Bearer $VAPI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "provider": "byo-phone-number", "number": "+15551234567", "credentialId": "" }' ``` Then assign your assistant to the number (inbound). Full BYO SIP trunk credential + phone-number reference (fields may change - this is Vapi's API). ## Troubleshooting | Status | Meaning | Fix | | ------------------- | ----------------- | --------------------------------------- | | `400` on `/trunks/` | Missing `sip_url` | Send `sip:sip.vapi.ai` | | `400` on attach | Number not in org | Use a valid `id` from **GET /numbers/** | | `401` | Bad Unpod token | Verify `Authorization: Token …` | | `403` | Wrong org | Verify the `Org-Handle` header | # Vapi (Dashboard) Source: https://docs.unpod.ai/telephony/integrations/vapi/dashboard Connect Vapi to Unpod with no code - configure the provider and route a number straight from the Unpod Studio. This is the **no-code** path. You create a SIP trunk on your Unpod number, copy its origin-endpoint credentials into Vapi as a **BYO SIP trunk**, then point a Vapi number and assistant at it. One trunk carries both inbound and outbound calls - no separate provider setup. Prefer code? See the [API guide](/telephony/integrations/vapi/api). **You need:** an Unpod number on a Bridge, and a Vapi account (Settings → **Integrations** access). Calls flow **Unpod trunk ⇄ Vapi** over SIP - Unpod routes outbound to `sip.vapi.ai`, and Vapi sends inbound to your Unpod endpoint. ## Part 1 - Create the trunk in Unpod In **Telephony**, select your number and click **Configure**. On the **New trunk** tab, set a **Name** you'll recognise and the **trunk origin endpoint**: * **SIP URI / address** - `sip.vapi.ai` (Vapi's inbound SIP host). * **Port** `5060` and **Transport** (`TCP` / `UDP` / `TLS`). * **Allowed IPs / CIDR** - optional source allow-list. Click **Test** to validate, then **Create Trunk**. This one trunk handles **both inbound and outbound** calls for the number. Unpod - configure a new trunk pointing at Vapi The number now shows **Linked · Vapi**. Under **Origin Endpoint Details** copy the values - you'll paste them into Vapi next: * **Address** - e.g. `sip-lb1.unpod.tel` * **Port** / **Protocol** - `5060` / `UDP` * **Username** and **Password** (under **Authentication**) Unpod - linked trunk origin endpoint credentials ## Part 2 - Register the trunk in Vapi In the Vapi dashboard go to **Settings → Integrations**, then open the **SIP Trunk** provider under **Phone Number Providers**. Vapi - Integrations, SIP Trunk provider On the SIP Trunk page click **Configure New SIP Trunk**. Vapi - SIP trunk list, configure new Give the trunk a **Name**, then under **Gateway #1** enter the Unpod values from Part 1: * **IP Address / Domain** - your Unpod address (e.g. `sip-lb1.unpod.tel`). * **Port** `5060`, **Outbound Protocol** `UDP`. * Tick **Allow inbound calls** and **Allow outbound calls**. * Under **Authentication**, paste the **Username** and **Password**. Vapi - add SIP trunk gateway + auth ## Part 3 - Attach a number and assistant Under **Phone Numbers → Create Phone Number**, pick **BYO SIP Trunk Number**, enter the phone number, and select your trunk under **SIP Trunk Credential**. Vapi - BYO SIP trunk number, select credential In **Inbound Settings**, confirm the **Inbound Phone Number** and open the **Assistant** dropdown. Vapi - inbound settings, assistant Choose the assistant that answers inbound calls. (Optionally route to a **Squad**, **Workflow**, or a **Fallback Destination** instead.) Vapi - assistant picker For outbound, go to **Outbound**, name the campaign, select your Unpod number, upload a contacts **CSV**, pick an **Assistant**, then **Launch campaign**. Vapi - outbound campaign ## Publish Back in the Unpod Studio, click **Publish** to activate the configuration. The number is then ready for inbound and outbound calls. ## Troubleshooting | Symptom | Likely cause | Fix | | ------------------------------- | ----------------------------- | -------------------------------------------------------------------------- | | Trunk **Test** fails in Unpod | Wrong SIP URI / transport | Use `sip.vapi.ai`; try `UDP`/`TCP` to match Vapi | | Vapi can't reach the trunk | Wrong gateway address or auth | Re-copy **Address** + **Username/Password** from the Unpod origin endpoint | | Call connects, assistant silent | No assistant bound | Set the **Assistant** in the number's **Inbound Settings** | | Inbound not arriving | Source IP blocked | Add Vapi's IPs to **Allowed IPs / CIDR** on the Unpod trunk | | Number not reachable | Config not published | Click **Publish** in the Unpod Studio | Same flow over the REST API. Back to the integrations overview. # WebSockets Source: https://docs.unpod.ai/telephony/integrations/websockets WebSockets integration for Unpod - raw audio streaming. Coming soon. **Coming soon.** WebSockets isn't a one-click provider in Unpod yet. Here's the planned model and what you can use today. **WebSockets** - Raw audio streaming. ## How it will work A documented **custom WebSocket** integration is on the roadmap for full control over raw audio framing.
📞 Call Inbound / outbound
WS frames
🔗 Unpod trunk Routing + failover
route
🔌 WebSockets agent Raw audio
## Available today The [WebSocket connectivity guide](/speech-stack/websocket) already covers streaming raw audio frames into the bridge today. ## Want it sooner? Ping us on Discord and we'll prioritise WebSockets. Vapi, LiveKit, Twilio and Daily are ready today. # Introduction Source: https://docs.unpod.ai/telephony/introduction Connect phone numbers, SIP trunks, and calls to your Unpod Voice AI agents The **Connectivity** APIs are the telephony control plane for Unpod. They let you bring your own carrier (BYO-SIP), map phone numbers to that carrier, route numbers to your Voice AI agents, and observe the per-number call lifecycle - all over a single REST surface. **Base URL:** `https://unpod.ai/api/v2/platform/` Every request needs an `Authorization: Token ` header and an `Org-Handle` header. See [Authentication](/api/get-started/authentication). ## The three building blocks Your phone numbers (DIDs). List the pool available to your org and attach a number to an agent so inbound calls reach it. A trunk is your SIP carrier credential. Create one, map numbers to it, and get back the carrier ingress (the *origin endpoint*). Observe the lifecycle: which numbers are linked, to which agent or carrier, and the projection sync state - plus full call logs. ## How it fits together ```mermaid theme={null} flowchart LR A["📞
Inbound Call"] e1@==>|① SIP INVITE| B["📡
SIP Carrier"] B e2@==>|② Route| C["🔗
Trunk
Origin Endpoint"] C e3@==>|③ Match| D["📱
DID / Number
E.164"] D e4@==>|④ Bridge| E["🤖
Voice AI Agent"] e1@{ animate: true } e2@{ animate: true } e3@{ animate: true } e4@{ animate: true } classDef ep fill:#5a4fff,color:#fff,stroke:#3d34d9,stroke-width:3px; classDef pp fill:#796cff,color:#fff,stroke:#5a4fff,stroke-width:2px; class A,E ep; class B,C,D pp; linkStyle default stroke:#9a90ff,stroke-width:2.5px; ``` Caller dials your DID number → SIP INVITE hits your carrier Carrier receives call → routes to your configured trunk endpoint Trunk matches incoming DID → bridges to the assigned agent Phone number (E.164 format) → maps to your Voice AI Agent Agent answers → conversation begins There is one primary termination path: | Path | What it does | Endpoint | | ----------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | **Leg A - BYO carrier** | Map a number to your SIP trunk; the carrier sends inbound calls to your origin endpoint. | [Attach Numbers to Trunk](/telephony/trunks/attach-numbers) | ## Key concepts * **DID/Number** - a phone number in E.164 format (e.g. `+15551234567`). * **Trunk** - a SIP carrier credential (`sip_url`, `transport`, `port`, auth, source-IP allow-list). Secrets are always masked in responses. * **Origin endpoint** - the shared SBC ingress (`sip:`) the carrier sends inbound calls to, plus the accepted source IPs and mapped DIDs. * **Bridge** - the routing entity numbers attach onto. It is **auto-resolved and hidden** on the Connectivity surface - you never manage it directly here. * **Partial success** - attach/detach operate on a list; each number reports `ok`/`error` independently. Create a trunk, map a number, and confirm the lifecycle - step by step. # List Numbers Source: https://docs.unpod.ai/telephony/numbers/list-numbers GET /api/v2/platform/telephony/numbers/ List the telephony numbers available to your organization Returns the numbers available to you. Behavior depends on the `Org-Handle` header: * **With Org-Handle** — returns your organization's own numbers (any state) plus the shared unassigned pool (`NOT_ASSIGNED`). * **Without Org-Handle** — returns only the shared unassigned pool. Use a number's `id` when attaching it to a [trunk](/telephony/trunks/attach-numbers). **Prerequisites:** API Token. See [Authentication](/api/get-started/authentication). ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | -------------------------- | | Authorization | string | Yes | `Token ` | | Org-Handle | string | No | Organization domain handle | ### Number object fields | Field | Type | Description | | ------ | ------- | ------------------------------------------- | | id | integer | Unique number id (use this in attach calls) | | number | string | Phone number in E.164 format | | state | string | `NOT_ASSIGNED` or `ASSIGNED` | | active | boolean | Whether the number is usable | ```json 200 theme={null} { "status_code": 200, "message": "Telephony numbers fetched successfully.", "data": [ { "id": 501, "number": "+15551234567", "state": "NOT_ASSIGNED", "active": true } ] } ``` ```bash cURL theme={null} curl -s "https://unpod.ai/api/v2/platform/telephony/numbers/" \ -H "Authorization: Token " \ -H "Org-Handle: " ``` # Quickstart Source: https://docs.unpod.ai/telephony/quickstart Bring your SIP carrier, map a number, and route a call in under 5 minutes This walkthrough takes you from zero to a number mapped onto your own SIP carrier. You'll need your **API Token** and **Org-Handle** - see [Authentication](/api/get-started/authentication). Every request uses these two headers (writes also send `Content-Type: application/json`): ```bash theme={null} export BASE="https://unpod.ai" export AUTH="Authorization: Token " export ORG="Org-Handle: " ``` Get your `Org-Handle` from [Get All Organizations](/api/space/organizations) - the `domain_handle` field. List the unassigned numbers in your org's pool. Note an `id` to use later. ```bash theme={null} curl -s "$BASE/api/v2/platform/telephony/numbers/" -H "$AUTH" -H "$ORG" ``` See [List Numbers](/telephony/numbers/list-numbers). ```bash theme={null} curl -s -X POST "$BASE/api/v2/platform/telephony/trunks/" \ -H "$AUTH" -H "$ORG" -H "Content-Type: application/json" \ -d '{ "name": "My Carrier Trunk", "sip_url": "sip:carrier.net", "auth_username": "user", "auth_password": "pass", "transport": "tcp", "port": "5060", "source_ips": ["1.2.3.4", "5.6.7.0/24"] }' ``` The response returns the new trunk `id` (e.g. `21`). Copy it. See [Create Trunk](/telephony/trunks/create-trunk). Replace `21` with your trunk id and `501` with a number id from Step 2. ```bash theme={null} curl -s -X POST "$BASE/api/v2/platform/telephony/trunks/21/attach-numbers/" \ -H "$AUTH" -H "$ORG" -H "Product-Id: unpod.dev" \ -H "Content-Type: application/json" \ -d '{"number_ids": [501]}' ``` The response includes the **origin endpoint** - the SBC ingress your carrier sends inbound calls to, plus the accepted source IPs. See [Attach Numbers to Trunk](/telephony/trunks/attach-numbers). ```bash theme={null} curl -s "$BASE/api/v2/platform/telephony/overview/" -H "$AUTH" -H "$ORG" ``` Each number shows `connection_state`, termination kind, agent link, and `sync_state`. See [Telephony Overview](/telephony/calls/overview). ## What next Step 1 - unmap numbers from the trunk, returning them to `NOT_ASSIGNED`. Step 2 - remove the trunk and its credential once numbers are detached. # Attach Numbers to Trunk Source: https://docs.unpod.ai/telephony/trunks/attach-numbers POST /api/v2/platform/telephony/trunks/{id}/attach-numbers/ Map one or more numbers to a SIP trunk and get the origin endpoint Map one or more numbers to this trunk - the **Leg-A** (BYO carrier) path. The bridge is auto-resolved and hidden. The response returns the trunk-level **origin endpoint**: the shared SBC ingress your carrier sends inbound calls to, the mapped DIDs, and the accepted source IPs. **Prerequisites:** API Token + Org-Handle. See [Authentication](/api/get-started/authentication). ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | ---------------------------------------------------------------- | | Authorization | string | Yes | `Token ` | | Org-Handle | string | Yes | Organization domain handle | | Product-Id | string | No | Product scope for the auto-resolved bridge (default `unpod.dev`) | | Content-Type | string | Yes | `application/json` | ### Path parameters | Name | Type | Required | Description | | ---- | ------- | -------- | ----------- | | id | integer | Yes | Trunk id | ### Request body | Field | Type | Required | Description | | ------------ | ---------- | -------- | ------------------------------------------- | | number\_ids | integer\[] | Yes | Numbers to map (deduped, order preserved) | | bridge\_slug | string | No | Explicit bridge; auto-resolved when omitted | | region | string | No | Region hint (e.g. `IN`) | ### Origin endpoint | Field | Type | Description | | --------------------- | --------- | ---------------------------------------- | | ingress | string | SBC ingress URI your carrier dials in to | | dids | string\[] | The numbers successfully mapped | | accepted\_source\_ips | string\[] | The trunk's source-IP allow-list | | region | string | Resolved bridge region | ### Partial success `201` if at least one number maps, else `400`. Each `data.numbers` entry reports `ok`/`error` independently. ```json 201 theme={null} { "status_code": 201, "message": "Numbers mapped to trunk.", "data": { "trunk_id": 21, "origin_endpoint": { "ingress": "sip:sip.unpod.tel", "dids": ["+15551234567"], "accepted_source_ips": ["1.2.3.4", "5.6.7.0/24"], "region": "us-east" }, "numbers": [ { "number_id": 501, "number": "+15551234567", "connection_state": "NOT_LINKED", "ok": true } ] } } ``` ```json 400 theme={null} { "status_code": 400, "message": "No numbers could be mapped.", "data": { "trunk_id": 21, "origin_endpoint": { "ingress": "sip:sip.unpod.tel", "dids": [], "accepted_source_ips": ["1.2.3.4"], "region": null }, "numbers": [ { "number_id": 501, "ok": false, "error": "Number not found or not available to this organization." } ] } } ``` ```bash cURL theme={null} curl -s -X POST "https://unpod.ai/api/v2/platform/telephony/trunks/21/attach-numbers/" \ -H "Authorization: Token " \ -H "Org-Handle: " \ -H "Product-Id: unpod.dev" \ -H "Content-Type: application/json" \ -d '{"number_ids": [501]}' ``` # Create Trunk Source: https://docs.unpod.ai/telephony/trunks/create-trunk POST /api/v2/platform/telephony/trunks/ Create a SIP trunk (carrier credential) for your organization Create a SIP trunk - your carrier credential. Once created, [map numbers to it](/telephony/trunks/attach-numbers) to receive inbound calls. The response masks `auth_password`. **Prerequisites:** API Token + Org-Handle. See [Authentication](/api/get-started/authentication). ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | -------------------------- | | Authorization | string | Yes | `Token ` | | Org-Handle | string | Yes | Organization domain handle | | Content-Type | string | Yes | `application/json` | ### Request body | Field | Type | Required | Description | | -------------- | --------- | -------- | ------------------------------------------- | | name | string | Yes | Display name for the trunk | | sip\_url | string | Yes | Carrier SIP URL / host | | auth\_username | string | No | SIP auth username | | auth\_password | string | No | SIP auth password (masked in responses) | | transport | string | No | `tcp` (default), `udp`, or `tls` | | port | string | No | SIP port (default `5060`) | | source\_ips | string\[] | No | Carrier source-IP allow-list (CIDR allowed) | ```json 201 theme={null} { "status_code": 201, "message": "Trunk created successfully.", "data": { "id": 21, "name": "My Carrier Trunk", "sip_url": "sip:carrier.net", "transport": "tcp", "port": "5060", "auth_username": "user", "auth_password": "pass", "allowed_ips": "1.2.3.4,5.6.7.0/24", "active": true, "org_handle": "acme.co" } } ``` ```json 400 theme={null} { "status_code": 400, "message": "Invalid trunk payload", "error": { "sip_url": ["This field is required."] } } ``` ```bash cURL theme={null} curl -s -X POST "https://unpod.ai/api/v2/platform/telephony/trunks/" \ -H "Authorization: Token " \ -H "Org-Handle: " \ -H "Content-Type: application/json" \ -d '{ "name": "My Carrier Trunk", "sip_url": "sip:carrier.net", "auth_username": "user", "auth_password": "pass", "transport": "tcp", "port": "5060", "source_ips": ["1.2.3.4", "5.6.7.0/24"] }' ``` Copy the returned `id` - you'll need it to attach numbers, fetch, or delete the trunk. # Delete Trunk Source: https://docs.unpod.ai/telephony/trunks/delete-trunk DELETE /api/v2/platform/telephony/trunks/{id}/ Delete a SIP trunk and its number bindings Delete a SIP trunk. This delegates to the credential teardown, which removes the trunk's number bindings. Returns `204 No Content` on success. Deleting a trunk removes its bindings. [Detach any mapped numbers](/telephony/trunks/detach-numbers) first if you want them cleanly returned to `NOT_ASSIGNED` with deprovisioning. **Prerequisites:** API Token + Org-Handle. See [Authentication](/api/get-started/authentication). ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | -------------------------- | | Authorization | string | Yes | `Token ` | | Org-Handle | string | Yes | Organization domain handle | ### Path parameters | Name | Type | Required | Description | | ---- | ------- | -------- | ----------- | | id | integer | Yes | Trunk id | ```text 204 theme={null} (No content) ``` ```json 404 theme={null} { "status_code": 404, "message": "Trunk not found." } ``` ```bash cURL theme={null} curl -s -X DELETE "https://unpod.ai/api/v2/platform/telephony/trunks/21/" \ -H "Authorization: Token " \ -H "Org-Handle: " ``` # Detach Numbers from Trunk Source: https://docs.unpod.ai/telephony/trunks/detach-numbers POST /api/v2/platform/telephony/trunks/{id}/detach-numbers/ Unmap one or more numbers from a SIP trunk Unmap one or more numbers from this trunk. Detaching deletes the number's bridge mapping, fires deprovision, sets the number back to `NOT_ASSIGNED`, and releases channels. **Prerequisites:** API Token + Org-Handle. See [Authentication](/api/get-started/authentication). ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | -------------------------- | | Authorization | string | Yes | `Token ` | | Org-Handle | string | Yes | Organization domain handle | | Content-Type | string | Yes | `application/json` | ### Path parameters | Name | Type | Required | Description | | ---- | ------- | -------- | ----------- | | id | integer | Yes | Trunk id | ### Request body | Field | Type | Required | Description | | ----------- | ---------- | -------- | ---------------- | | number\_ids | integer\[] | Yes | Numbers to unmap | ### Partial success Each `data.numbers` entry reports `ok`/`error`. A number that isn't mapped to this trunk returns `ok: false` with `"Number is not mapped to this trunk."` ```json 200 theme={null} { "status_code": 200, "message": "Numbers unmapped from trunk.", "data": { "trunk_id": 21, "numbers": [ { "number_id": 501, "ok": true } ] } } ``` ```json 404 theme={null} { "status_code": 404, "message": "Trunk not found." } ``` ```bash cURL theme={null} curl -s -X POST "https://unpod.ai/api/v2/platform/telephony/trunks/21/detach-numbers/" \ -H "Authorization: Token " \ -H "Org-Handle: " \ -H "Content-Type: application/json" \ -d '{"number_ids": [501]}' ``` # Get Trunk Source: https://docs.unpod.ai/telephony/trunks/get-trunk GET /api/v2/platform/telephony/trunks/{id}/ Fetch a single SIP trunk by id Fetch a single SIP trunk by its `id`. Only trunks owned by your organization are returned - anything else is `404`. Secrets are masked. **Prerequisites:** API Token + Org-Handle. See [Authentication](/api/get-started/authentication). ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | -------------------------- | | Authorization | string | Yes | `Token ` | | Org-Handle | string | Yes | Organization domain handle | ### Path parameters | Name | Type | Required | Description | | ---- | ------- | -------- | ----------- | | id | integer | Yes | Trunk id | ```json 200 theme={null} { "status_code": 200, "message": "Trunk fetched successfully.", "data": { "id": 12, "name": "Acme Primary Trunk", "sip_url": "sip:sip.acme-voice.com", "transport": "tcp", "port": "5060", "auth_username": null, "auth_password": null, "allowed_ips": "", "active": true, "org_handle": "acme.co" } } ``` ```json 404 theme={null} { "status_code": 404, "message": "Trunk not found." } ``` ```bash cURL theme={null} curl -s "https://unpod.ai/api/v2/platform/telephony/trunks/12/" \ -H "Authorization: Token " \ -H "Org-Handle: " ``` # List Trunks Source: https://docs.unpod.ai/telephony/trunks/list-trunks GET /api/v2/platform/telephony/trunks/ List your organization's SIP trunks List the SIP trunks (carrier credentials) owned by your organization, newest first. Secrets (`auth_password`) are masked in the response. **Prerequisites:** API Token + Org-Handle. See [Authentication](/api/get-started/authentication). ### Headers | Name | Type | Required | Description | | ------------- | ------ | -------- | -------------------------- | | Authorization | string | Yes | `Token ` | | Org-Handle | string | Yes | Organization domain handle | ### Trunk object fields | Field | Type | Description | | -------------- | ------- | ------------------------------------------ | | id | integer | Trunk id (use in attach / detach / delete) | | name | string | Display name | | sip\_url | string | Carrier SIP URL / host | | transport | string | `tcp`, `udp`, or `tls` | | port | string | SIP port (default `5060`) | | auth\_username | string | SIP auth username (nullable) | | auth\_password | string | Masked - only the last 4 chars shown | | allowed\_ips | string | Comma-separated source-IP allow-list | | active | boolean | Whether the trunk is active | | org\_handle | string | Owning organization handle | ```json 200 theme={null} { "status_code": 200, "message": "Trunks fetched successfully.", "data": [ { "id": 12, "name": "Acme Primary Trunk", "sip_url": "sip:sip.acme-voice.com", "transport": "tcp", "port": "5060", "auth_username": null, "auth_password": null, "allowed_ips": "", "active": true, "org_handle": "acme.co" } ] } ``` ```bash cURL theme={null} curl -s "https://unpod.ai/api/v2/platform/telephony/trunks/" \ -H "Authorization: Token " \ -H "Org-Handle: " ```