{"openapi":"3.0.0","info":{"title":"Telzino API","version":"1.0.0","description":"API documentation for Telzino"},"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"CallLog":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Unique call log identifier"},"agent_id":{"type":"string","format":"uuid","description":"Agent that handled the call"},"organization_id":{"type":"string","format":"uuid","description":"Organization the call belongs to"},"room_name":{"type":"string","description":"Room identifier for the call"},"transcript_text":{"type":"string","description":"Plain text version of the transcript"},"transcript_json":{"type":"array","description":"Structured transcript. `timestamp` is Unix epoch seconds for when the line was added: for a caller line, after the caller finished and the speech was transcribed; for an agent line, when the agent finished speaking. Use speech_started_at / speech_ended_at / transcript_final_at for the actual moments.\n","items":{"type":"object","properties":{"speaker":{"type":"string","enum":["agent","user","system"]},"text":{"type":"string"},"timestamp":{"type":"number"},"is_partial":{"type":"boolean"},"type":{"type":"string","enum":["webhook","tool_call"],"description":"Present on webhook and tool-call lines"},"speech_started_at":{"type":"number","description":"Unix seconds when this person started talking (TLZ-584; newer calls)"},"speech_ended_at":{"type":"number","description":"Unix seconds when this person stopped talking"},"transcript_final_at":{"type":"number","description":"Caller lines — Unix seconds when the final transcript was ready"},"latency":{"type":"object","description":"Agent lines only. Reply timing in ms — total_ms (LiveKit's caller wait: stopped talking → agent audio started; absent when LiveKit did not measure it), end_of_turn_ms, transcription_ms and turn_hook_ms (all three on the first reply of a turn only), retrieval_ms, llm_first_token_ms, llm_ms, llm_calls, tools_ms, tools[{name, ms}], tts_first_audio_ms, spans[{stage, start_ms, end_ms, first_token_ms?, name?}] — offsets from when the caller stopped talking, chronological, stage one of end_of_turn, transcription, turn_hook, retrieval, llm, tool, speech_synthesis — slowest_stage, interrupted.\n"},"tool_call":{"type":"object","description":"Tool-call lines — name, arguments, result, error?, duration_ms?"}}}},"entry_count":{"type":"integer","description":"Number of transcript entries"},"call_duration":{"type":"integer","description":"Call duration in seconds"},"call_started_at":{"type":"string","format":"date-time","description":"When the call started"},"call_ended_at":{"type":"string","format":"date-time","description":"When the call ended"},"session_data":{"type":"object","description":"Additional session metadata (caller_id, sip_call_id, direction, and latency — this call's caller-wait summary {version, turns, p50_ms, p95_ms, max_ms} when measured)\n"},"summary_text":{"type":"string","nullable":true,"description":"AI-generated call summary. Encrypted transcripts/summaries are decrypted before being returned."},"sentiment":{"type":"string","nullable":true,"description":"Overall call sentiment (e.g. Positive, Neutral, Negative)"},"resolution_status":{"type":"string","nullable":true,"description":"Call outcome (resolved, transferred, abandoned, ...)"},"escalation_reason":{"type":"string","nullable":true,"description":"Why the call was escalated/transferred, when applicable"},"recording_url":{"type":"string","nullable":true,"description":"Present when a recording exists; download via GET /v1/call-logs/{callLogId}/recording"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"UpdateAgentRequest":{"type":"object","description":"Request body for updating an agent. All fields are optional - only include fields you want to update.","properties":{"organizationId":{"type":"string","format":"uuid","description":"Optional org guard. When provided, the request is rejected with 404\nunless the target agent already belongs to this organization. It does\nNOT move the agent between organizations - it only scopes the update\nso a caller cannot accidentally modify an agent in a sibling org under\nthe same account. Omit it to keep the default account-scoped behavior.\n"},"name":{"type":"string","minLength":1,"maxLength":255,"description":"Display name of the agent"},"description":{"type":"string","description":"Description of what the agent does"},"greetingMessage":{"type":"string","description":"Initial message the agent says when call starts"},"outboundGreetingMessage":{"type":"string","description":"Greeting spoken when this agent places an outbound call"},"outboundStaticGreeting":{"type":"string","description":"Static line spoken verbatim before the outbound greeting (warnings/advisories, not alterable by AI)"},"aiDisclosure":{"type":"object","nullable":true,"description":"TLZ-477 US-4.3 AI-agent disclosure — the line telling the callee they are speaking to an AI. Null (or omitted) means none is configured. Spoken by the outbound worker, which records the outcome on the call as `disclosure_delivered` and `disclosure_timestamp` and echoes both in the result webhook.\n","required":["enabled"],"properties":{"enabled":{"type":"boolean","description":"Required and explicit — a compliance control is never defaulted."},"text":{"type":"string","maxLength":1000,"description":"The line spoken to the callee. Required when `enabled` is true.","example":"Just so you know, I'm an AI assistant calling on behalf of Acme."},"position":{"type":"string","enum":["after_greeting"],"default":"after_greeting","description":"Where the line lands in the call. Only `after_greeting` is implemented; other values are rejected rather than silently ignored."},"required_states":{"type":"array","items":{"type":"string","example":"CA"},"description":"US state/territory codes where disclosure is legally mandatory. Accepted case-insensitively, stored upper-case and de-duplicated. NOTE: stored as configuration only — nothing in the API evaluates it, since matching a callee to a state requires an area-code-to-state map that does not exist yet.\n"}}},"contactPolicy":{"type":"object","nullable":true,"description":"TLZ-477 compliance policy for this agent. Null (or omitted) means no policy: nothing is required and nothing is rejected. Unknown keys are rejected, so a typo is a 400 rather than a control that silently does nothing. Distinct from a retry policy's `contact_policy`, which holds per-number attempt caps.\n\nMERGED key by key onto the stored policy, not replaced. The keys are independent compliance controls, so sending `{\"require_consent_basis\": true}` on an agent that already has `calling_windows` leaves that window in place. Send `contactPolicy: null` to clear the whole policy.\n","properties":{"require_consent_basis":{"type":"boolean","description":"When true, Trigger Outbound Call for this agent must supply a `consent_basis` or it is rejected with 400."},"abandon_rate_alert_threshold":{"type":"number","minimum":0,"maximum":1,"default":0.03,"description":"TLZ-477 US-4.4 — the abandon rate above which this agent is flagged as breaching in GET /v1/metrics. Defaults to the FCC 3% limit when absent. An explicit 0 is meaningful (flag any abandoned call) and is not treated as unset.\n"},"calling_windows":{"type":"object","description":"TLZ-477 US-4.2 calling hours, evaluated on the CALLEE's clock. Absent means no hours restriction. Windows cannot wrap midnight, and `end_local` is exclusive, so \"21:00\" means \"before 9pm\".\n","properties":{"start_local":{"type":"string","example":"08:00","description":"Inclusive opening, 24-hour HH:MM, callee-local"},"end_local":{"type":"string","example":"21:00","description":"Exclusive close, 24-hour HH:MM, callee-local"},"timezone_source":{"type":"string","enum":["callee_npanxx","agent_timezone","explicit"],"description":"Which clock to read. `callee_npanxx` infers it from the destination area code (area-code granularity, so area codes spanning two zones resolve to the dominant one); `agent_timezone` uses the agent's own timezone; `explicit` uses the `timezone` field, which is then required.\n"},"timezone":{"type":"string","example":"America/Denver","description":"IANA zone; required when timezone_source is explicit"}}}}},"amdConfig":{"oneOf":[{"type":"boolean"},{"type":"object"}],"description":"Answering Machine Detection for outbound calls. Pass `true` to enable with defaults, `false` to disable, or an object to tune it. Unrelated to `callDetection` (mobile-number lookup). Object keys: `enabled` (boolean), `sensitivity` (fast|balanced|precise), `max_detection_ms` (int, null = use preset), `on_machine` (leave_voicemail|hang_up|continue_as_human), `on_silence` (reprompt|hang_up|continue_as_human), `uncertain_prompt` (string), `voicemail_max_duration_ms` (int), `voicemail_beep_wait_ms` (int).\n"},"ivrNavigation":{"oneOf":[{"type":"boolean"},{"type":"object"}],"description":"IVR menu navigation for outbound calls. Pass `true` to enable with defaults, `false` to disable, or an object to tune it. Object keys: `enabled` (boolean), `target_intent` (reach_human|reach_department|custom), `target_department` (string), `spoken_keywords` (string[]), `max_attempts` (int), `max_duration_ms` (int), `fallback_action` (press_zero|hang_up|wait_for_human), `hold_max_wait_ms` (int). REPLACES the stored object wholesale — it is a jsonb blob, not a merge, so send every key you want to keep, not only the ones you are changing. `dtmf_transport` is NOT accepted and returns 400: DTMF transport (rfc2833 vs SIP INFO) is a trunk-level setting that LiveKit's publish_dtmf does not expose, so accepting it would silently do nothing. Requires AMD: navigation is started by the answering-machine verdict, so it can never run with AMD off. Enabling it while AMD is off returns 400.\n"},"voicemailMessage":{"type":"string","description":"Message spoken when the agent reaches an answering machine"},"voicemailMessageAudioUrl":{"type":"string","description":"Pre-recorded voicemail audio; played instead of voicemailMessage when set"},"aiGreetingPrompt":{"type":"string","nullable":true,"description":"Prompt used to generate a dynamic AI greeting; null to clear"},"systemPrompt":{"type":"string","description":"System prompt defining agent behavior"},"notetakerConfig":{"$ref":"#/components/schemas/NotetakerConfig"},"agentType":{"type":"string","enum":["simple","advanced","custom","appointment_scheduler","receptionist","message_taker","blank_slate","outbound","opentable_reservation","translator","notetaker"],"description":"Under Feature Administration, CHANGING this to `receptionist`, `appointment_scheduler`, `message_taker`, `blank_slate`, `opentable_reservation`, `notetaker` or `outbound` is refused with 403 when the matching Template is locked. Re-sending the agent's current value is never refused, so a read-modify-write round trip still works for every value this enum accepts. Changing an existing agent to or from `notetaker` is refused with 400; create a new agent instead. Changing it to `translator` (TLZ-598) also needs interpreter settings, sent as `interpreterConfig` or already stored; without them the request is refused with 400. A translator must stay on `ttsModel: cartesia`; any other vendor is refused with 400 (`voice_provider`). Changing it to `translator` also sets `detached: true` on the agent (the layout Connect shows translator settings in); re-sending `translator` on an agent that already is one leaves `detached` unchanged.\n"},"interpreterConfig":{"allOf":[{"$ref":"#/components/schemas/InterpreterConfig"}],"nullable":true,"description":"Translator agents (TLZ-598). Sending it REPLACES the stored settings with this complete, validated object; omitting it leaves them unchanged. `null` clears them, and only on an agent that is not a translator. A translator's `language` is set to `party_a_language`.\n"},"model":{"type":"string","description":"LLM model (e.g., gpt-4o, gpt-4.1)"},"voiceModel":{"type":"string","description":"Voice/speech model"},"sttModel":{"type":"string","description":"Speech-to-text model (e.g., deepgram)"},"ttsModel":{"type":"string","description":"Text-to-speech provider (cartesia, minimax)"},"voiceSettings":{"type":"object","description":"Voice configuration for text-to-speech. Supported providers: Cartesia, Minimax.\n- Cartesia: voice_id is UUID format (e.g., '79a125e8-cd45-4c13-8a67-188112f4dd22' for British Lady)\n- Minimax: voice_id is model name (e.g., 'English_radiant_girl'), plus speed and model fields\nTranslator agents (TLZ-598) run on the Cartesia sonic-3 family: when this request\nsends voiceSettings or ttsModel, or changes agentType to `translator`, a Cartesia\nmodel that is missing or not in the sonic-3 family is set to `sonic-3` (not\nrefused), and a missing voice_id gets the template's default voice.\n- language_voices.es (optional): the voice used when the caller speaks Spanish,\n  `{ tts_model, voice_id, model?, speed?, language? }`. Needs language `multi`\n  (not sttModel `cartesia`). voiceSettings is stored whole, so omit it to remove it.\n","example":{"voice_id":"79a125e8-cd45-4c13-8a67-188112f4dd22"}},"config":{"type":"object","description":"General agent configuration for audio settings.\n- ambient_type: Background ambience (e.g., 'office_ambience')\n- ambient_volume: Volume 0.0-1.0 (default 0.3)\n- thinking_sounds_enabled: Enable thinking sounds\n- thinking_volume: Volume 0.0-1.0 (default 0.5)\n- background_audio_enabled: Enable background audio\n- confirm_transfers: Validate a transfer destination before bridging (BLF when the call arrived over registered SIP, otherwise a keypress)\n- transfer_ring_timeout_seconds: Seconds a transfer destination rings before the agent stops trying (10-120, default 25). Applies to warm transfers, confirm_transfers keypress checks and web widget transfers; normal transfers ring for as long as the phone system allows\n- dtmf_transfer_enabled: Let the caller reach a transfer destination by keying it, so a rule transferring to pbx:100 also fires when they press 100. Applies to call transfer and warm transfer rules. Does not control whether the agent listens for keypresses at all — the agent's own instructions do\n- transfer_announcement_enabled: Set to false to silence the platform's spoken line right before a transfer (defaults to true)\n- transfer_announcement: Your own short sentence (max 97 characters, the default line's length plus 20) spoken instead of the platform's transfer line\n- response_filler_enabled: Speak one short filler line when the caller is left waiting on a tool call (defaults to false; never during transfers or ending the call)\n- response_filler_delay_seconds: Seconds of silence before the filler (0.5-10, default 1.5)\n- response_filler_phrases: Up to 10 filler lines of max 200 characters, one picked at random (empty uses \"One moment, let me check that for you.\")\n","example":{"ambient_type":"office_ambience","ambient_volume":0.3,"thinking_sounds_enabled":true,"thinking_volume":0.5,"confirm_transfers":false,"dtmf_transfer_enabled":false}},"extensions":{"type":"object","description":"Extensions configuration for webhooks, MCP servers, tools, and parameters.\n- webhooks: Array of webhook configurations\n- mcpServers: Array of MCP server integrations. Each may carry\n  `allowed_tool_names` to limit which of the server's tools the agent can\n  use; omitted or empty means every tool. Because `extensions` is a full\n  replace, an update that sends an MCP entry without `allowed_tool_names`\n  removes that connection's limit; omitting `extensions` entirely changes\n  nothing. Each may also carry `pre_answer_calls`: tools the worker calls\n  itself while the phone rings, `[{ tool_name, arguments }]`, where string\n  arguments may use {{caller_id}}, {{agent_id}}, {{direction}},\n  {{room_name}}, {{timestamp}}. Each tool must be one the agent may use and\n  may appear once; keep it to 1-2, since the AI greeting waits for all of\n  them. The same full-replace rule applies.\n- tools: Array of custom tools\n- parameters: Array of global parameters\n\nUnder Feature Administration, a request that ADDS the `ns_send_sms` or\n`ns_check_sms_replies` tool is refused with 403 when SMS Messaging is\nlocked for the agent's organization. The gate is evaluated on the CHANGE,\nso re-sending tools the agent already has is never refused, and removing an\nSMS tool is always allowed.\n\n`tools[].type` accepts the internal types a GET returns (`netsapiens`,\n`twilio_sms`, `hosted_sms`, `hosted_sms_replies`, `schedule`, `teammate`,\n`teams_voicemail`, `email_tool`, `listings_search`), and\n`tools[].condition` additionally accepts the editor-written\n`participant_connected` and `end_of_call`, so `extensions.tools` from a GET\ncan be sent back.\n\nA `listings_search` tool takes the same `parameters` as on create:\n`listings_url` and `attested_by` (`attested_at` is set by the server). An\nAppFolio listings page or any other public https listings page works once\nattested; its listings are read automatically after the save. Adding the\ntool, or changing its `listings_url`, or switching\n`fetch_detail_pages` on, is refused with 403 when Listings Search is locked\nand needs a fresh attestation. Re-sending the stored tool unchanged is never\nrefused by the lock and keeps the stored attestation, but it is still\nvalidated: a stored tool that no longer passes the listings rules (for example\nan address the URL policy now refuses, or another tool named\n`search_listings` or `send_listing_link`) is refused with 400 until it is\ncorrected or removed. The create rules apply: `refresh_minutes` 10, 15, 30\nor 60; `business_name` optional, at most 100 characters, the sender name in\ntexts; `sms_provider` `twilio`, `netsapiens` or empty; `sms_sender_extension`\n2 to 10 digits or empty; `enabled: false` refused; one `listings_search` tool per\nagent; `description` at most 4,000 characters.\n\nHosted SMS: `send_hosted_sms` (type `hosted_sms`) texts the caller from the\nnumber in its `from_number` parameter, which must be one of the\norganization's hosted SMS numbers. A new or changed `from_number` is checked\nagainst those numbers when this endpoint saves it — an unowned number is\nrefused with 400, and an unchanged stored number is never re-checked. The\nsame number is checked again when the agent sends; an unknown or missing\nnumber makes the send fail. `check_hosted_sms_replies` (type\n`hosted_sms_replies`) reads the caller's text replies. Under Feature\nAdministration, a request that ADDS either tool is refused with 403 while\nHosted SMS is locked for the organization; re-sending, editing (including\n`from_number`) and removing them is not.\n\n`tools[].parameters` must be an array of `{key, value}` strings. The other\nshapes the dashboard templates store (the Zendesk template's parameters\nobject; the Freshdesk templates' numeric values with a `type` key) are\naccepted only when that whole stored tool is sent back exactly as GET\nreturned it, so the tool itself cannot be edited here. Parameters in any\nother shape are rejected, including `{name, value}` items and flow-builder\ncustom function tools (`{name, type, description, required}` items).\n\nA `payment` tool is rejected with 400. Because `extensions` is a full\nreplace, removing it from `tools` turns card payments off. Re-enable them\nwith PUT /v1/agents/{agentId}/payments/tool, re-sending `success_url`,\n`max_single_charge_cents`, `max_per_call_cents` and `max_attempts_per_call`\n(read from GET /v1/agents/{agentId}/payments/readiness); omitted ones reset\nto their defaults.\n","example":{"webhooks":[{"name":"CRM_Update","friendlyName":"Update CRM After Call","url":"https://api.example.com/webhook","method":"POST","condition":"after_call","headers":[{"key":"Authorization","value":"Bearer api-key"}],"parameters":[{"key":"transcript","value":"{{transcript}}"}]}],"parameters":[{"name":"company_name","type":"String","value":"Acme Corp"}]}},"integrations":{"type":"object","description":"Third-party integration configs (modmed, googleCalendar, outlookCalendar, twilio, netsapiens).\n\nUpdate semantics (differs from `extensions`, which is full-replace):\n- Field-level merge per provider: only the fields you send change; omitted fields are preserved.\n- Set a field to null to clear it. Set a whole provider to null to remove it.\n- Unknown provider keys are stored as-is.\n\nBehavior notes:\n- Configs are stored as-is; credentials are NOT validated against the provider.\n  ModMed/NetSapiens entries without valid tokens will not function at runtime\n  until tokens are supplied or the integration is connected via the dashboard.\n- Unlike the dashboard flow, writing `twilio` here does NOT auto-create the\n  send_sms_via_twilio tool in extensions — add it yourself if you want SMS.\n- Secret fields (passwords, authToken, client_secret, access/refresh tokens)\n  are write-only: they are never returned by any agent endpoint.\n- `twilio` credentials are NOT set here (TLZ-579). An organization has ONE Twilio\n  connection, saved in the Dashboard or Connect from any of its agents\n  (Integrations → Twilio); every agent in the organization uses it, and\n  rotating it there updates them all. A PUT whose `twilio` (or `Twilio`) block\n  sets or changes `accountSid`, `authToken`, `apiKeySid` or `apiKeySecret`\n  is rejected with 400. Sending back the values a GET returned is not a\n  change, so a GET → PUT round trip still succeeds.\n- `twilio.fromNumber` stays per agent and is OPTIONAL since TLZ-581. The Send\n  SMS tool carries its own From number in `extensions.tools[].parameters` as\n  `{\"key\": \"from_number\", \"value\": \"+1...\"}`, and that value is used in\n  preference to this one. `fromNumber` here remains the source for agents that\n  text WITHOUT a Send SMS tool (reservation booking links), and the fallback\n  for agents created before TLZ-581. A request that adds or changes a\n  `twilio_sms` tool with no From number in either place is rejected with 400.\n- `twilio.accountType` and `twilio.accountStatus` are read-only facts recorded\n  by the dashboard's free account check. Values sent here are ignored.\n","example":{"twilio":{"fromNumber":"+15551234567"},"modmed":null}},"transferEnabled":{"type":"boolean","description":"Enable SIP call transfer"},"webTransferEnabled":{"type":"boolean","description":"Enable web-call transfer. Only takes effect when call transfer is enabled; otherwise stored as false. When omitted on an update that turns transferEnabled on, it is turned on too."},"teammateEnabled":{"type":"boolean","description":"Enable Teams teammate integration"},"teammateVoicemailTargets":{"type":"array","description":"Teams users whose voicemail the agent can reach","items":{"type":"object","required":["name","upn"],"properties":{"name":{"type":"string"},"upn":{"type":"string","description":"User principal name (user@domain)"}}}},"emailServerId":{"type":"string","format":"uuid","nullable":true,"description":"Email server for the email tool; must belong to the account (see GET /v1/email-servers). Null to clear."},"pronunciations":{"type":"array","description":"Custom word pronunciations. Requires a TTS voice that supports them; MiniMax voices are English-only for pronunciations.","items":{"type":"object"}},"emailToolEnabled":{"type":"boolean","description":"Enable email sending during calls"},"emailToolInstructions":{"type":"string","description":"Instructions for when/how to send emails"},"callDetection":{"type":"boolean","description":"Enable mobile phone detection"},"echoDetection":{"type":"boolean","description":"Enable echo detection during calls"},"detectAutomatedCallers":{"type":"boolean","description":"Enable AI-based automated caller detection during calls"},"enableRecording":{"type":"boolean","description":"Enable call recording"},"sendRecordingLinkWithSummary":{"type":"boolean","description":"Include a link to the call recording in the post-call summary notification. Requires call recording to be enabled to have any effect."},"language":{"type":"string","description":"Agent language: en-US, es-US, or multi (multilingual)"},"timezone":{"type":"string","description":"Agent timezone in IANA format (e.g., America/New_York)"},"transferRules":{"type":"string","description":"Rules and conditions for call transfers"},"warmTransferEnabled":{"type":"boolean","description":"Enable warm transfer routing for this agent"},"warmTransferRules":{"type":"string","description":"Rules and instructions for warm transfers"},"warmTransferHandoffInstructions":{"type":"string","description":"What the AI says to the human agent during a warm transfer consult leg. Blank uses the built-in briefing."},"warmTransferMohUrl":{"type":"string","format":"uri","description":"Public URL of the hold-music file played to the caller during warm transfer. Obtain one by uploading a file via POST /v1/agents/{agentId}/moh, or set directly if you already host the audio. Leave empty for built-in hold music."},"warmTransferPassThroughCallerNumber":{"type":"boolean","description":"Whether to pass the original caller's number through to the transfer destination. Default is true."},"knowledgebaseLinks":{"type":"array","items":{"type":"string"},"description":"Array of URLs the agent references for knowledge. This is a legacy\nfield and is NOT the RAG Knowledge Base — for vector search use\nragEnabled and knowledgeBaseId below.\n"},"ragEnabled":{"type":"boolean","description":"Enables live vector search (RAG) against the assigned Knowledge Base\nduring calls. Requires knowledgeBaseId to be set — an agent with no\nKnowledge Base does not retrieve regardless of this flag. Returns\n403 forbidden when Knowledge Base is not enabled for the agent's\norganization. Setting it to false is never blocked.\n"},"knowledgeBaseId":{"type":"string","format":"uuid","nullable":true,"description":"The single organization-level Knowledge Base this agent retrieves\nfrom. Must belong to the same organization as the agent, otherwise\nthe request is rejected with 400. Set to null to unassign. List\navailable ids via GET /v1/organizations/{organization_id}/knowledge-bases.\n"},"ragConfig":{"type":"object","description":"RAG retrieval tuning. Unknown keys are rejected.","properties":{"top_k":{"type":"integer","minimum":1,"maximum":50,"description":"Number of chunks to retrieve per query."},"rewrite_query":{"type":"boolean","description":"Rewrite the caller's phrasing into a search query before retrieving."},"max_context_tokens":{"type":"integer","minimum":1,"maximum":200000,"description":"Cap on retrieved context injected into the prompt."},"fallback_message":{"type":"string","maxLength":2000,"description":"What the agent says when retrieval returns nothing useful."}}},"googlePlaceId":{"type":"string","nullable":true,"description":"Google Place ID linking the agent to a Google Business Profile listing\n(TLZ-426). The platform fetches the listing's details (hours, location,\nphone, website, services, ratings) and the agent uses them as verified\nfacts on calls. Set to null or \"\" to unlink. Changing or clearing the id\nalso clears the cached listing data (google_place_data,\ngoogle_place_context, google_place_fetched_at) so stale details from the\nprevious place are never served; the refresh service re-fetches the new\nlisting. Re-sending the stored id is a no-op.\n","example":"ChIJVS6XxJPFU4gRoE8c8ql2lCQ"},"email":{"type":"array","items":{"type":"string"},"description":"Email addresses for notifications and email tool"},"status":{"type":"string","enum":["active","inactive","cancelled","pending"],"description":"Agent activation status"}},"example":{"name":"Updated Support Agent","systemPrompt":"You are an updated customer support agent...","voiceSettings":{"voice_id":"79a125e8-cd45-4c13-8a67-188112f4dd22","speed":1.1},"config":{"thinking_sounds_enabled":false},"extensions":{"tools":[{"name":"send_slack_notification","type":"webhook","friendlyName":"Slack_Notification","description":"When the caller agrees to a demo, send their name, phone, and requested time.","url":"https://hooks.slack.com/services/T.../B.../xxxxxxxxxxxxxxxx","method":"POST","condition":"during_call","headers":[{"key":"Content-Type","value":"application/json"}],"parameters":[{"key":"text","value":"The text to post to Slack, composed by the agent from the conversation."}]}]},"status":"active"}},"Agent":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Unique agent identifier"},"organization_id":{"type":"string","format":"uuid","description":"Organization the agent belongs to"},"provider":{"type":"string","description":"Agent provider"},"name":{"type":"string","description":"Display name of the agent"},"description":{"type":"string","description":"Description of what the agent does"},"greeting_message":{"type":"string","description":"Initial message the agent says when call starts"},"outbound_greeting_message":{"type":"string","description":"Greeting spoken when this agent places an outbound call"},"outbound_static_greeting":{"type":"string","description":"Static line spoken verbatim before the outbound greeting (warnings/advisories, not alterable by AI)"},"system_prompt":{"type":"string","description":"System prompt defining agent behavior and personality"},"agent_type":{"type":"string","enum":["simple","advanced","custom","appointment_scheduler","receptionist","message_taker","blank_slate","outbound","opentable_reservation","translator","notetaker"],"description":"Type of agent configuration"},"interpreter_config":{"allOf":[{"$ref":"#/components/schemas/InterpreterConfig"}],"nullable":true,"description":"The Translator agent's settings (TLZ-598), returned exactly as stored. Null unless interpreter settings were ever saved for this agent -- sending `interpreterConfig` stores it on ANY agent type, not only `translator`, and an agent changed away from `translator` keeps its last settings here, unused. Required (and validated) when `agent_type` is `translator`.\n"},"notetaker_config":{"$ref":"#/components/schemas/NotetakerConfig"},"model":{"type":"string","description":"AI model used (e.g., gpt-4o)"},"voice_model":{"type":"string","description":"Voice model for real-time speech"},"stt_model":{"type":"string","description":"Speech-to-text model"},"tts_model":{"type":"string","description":"Text-to-speech model"},"voice_settings":{"$ref":"#/components/schemas/VoiceSettings"},"config":{"$ref":"#/components/schemas/AgentConfig"},"extensions":{"$ref":"#/components/schemas/Extensions"},"transfer_enabled":{"type":"boolean","description":"Whether SIP call transfer is enabled"},"web_transfer_enabled":{"type":"boolean","description":"Whether web-call transfer is enabled (only meaningful when transfer_enabled is true)"},"amd_config":{"type":"object","nullable":true,"description":"Answering Machine Detection configuration for outbound calls"},"voicemail_message":{"type":"string","nullable":true,"description":"Message spoken when the agent reaches an answering machine"},"voicemail_message_audio_url":{"type":"string","nullable":true,"description":"Pre-recorded voicemail audio URL; played instead of voicemail_message when set"},"ai_greeting_prompt":{"type":"string","nullable":true,"description":"Prompt used to generate a dynamic AI greeting"},"rag_enabled":{"type":"boolean","description":"Whether live Knowledge Base retrieval (RAG) is enabled"},"rag_config":{"type":"object","description":"RAG retrieval tuning (top_k, rewrite_query, max_context_tokens, fallback_message)"},"knowledge_base_id":{"type":"string","format":"uuid","nullable":true,"description":"Knowledge Base the agent retrieves from"},"email_server_id":{"type":"string","format":"uuid","nullable":true,"description":"Email server used by the email tool (see GET /v1/email-servers)"},"pronunciations":{"type":"array","nullable":true,"description":"Custom word pronunciations for the TTS voice","items":{"type":"object"}},"teammate_enabled":{"type":"boolean","description":"Whether Teams teammate integration is enabled"},"teammate_voicemail_targets":{"type":"array","description":"Teams users whose voicemail the agent can reach","items":{"type":"object","properties":{"name":{"type":"string"},"upn":{"type":"string"}}}},"email_tool_enabled":{"type":"boolean","description":"Whether the agent can send emails during calls"},"email_tool_instructions":{"type":"string","description":"Instructions for when/how the agent sends emails"},"call_detection":{"type":"boolean","description":"Enable mobile phone detection before calls start"},"echo_detection":{"type":"boolean","description":"Whether echo detection is enabled"},"detect_automated_callers":{"type":"boolean","description":"Whether AI-based automated caller detection is enabled"},"enable_recording":{"type":"boolean","description":"Whether call recording is enabled"},"send_recording_link_with_summary":{"type":"boolean","description":"Whether a link to the call recording is included in the post-call summary notification"},"language":{"type":"string","description":"Agent language (en-US, es-US, or multi)"},"timezone":{"type":"string","description":"Agent timezone in IANA format"},"transfer_rules":{"type":"string","description":"Rules and conditions for call transfers"},"warm_transfer_enabled":{"type":"boolean","description":"Whether warm transfer is enabled for this agent"},"warm_transfer_rules":{"type":"string","description":"Rules and instructions for warm transfers"},"warm_transfer_handoff_instructions":{"type":"string","description":"What the AI says to the human agent during a warm transfer consult leg. Blank uses the built-in briefing."},"warm_transfer_moh_url":{"type":"string","format":"uri","description":"Public URL of the uploaded MP3 or WAV file played to the caller on hold during warm transfer"},"warm_transfer_pass_through_caller_number":{"type":"boolean","description":"Whether to pass the original caller's number through to the transfer destination. Default is true."},"google_place_id":{"type":"string","nullable":true,"description":"Linked Google Place ID (echoes the value set via create/update)."},"google_place_data":{"type":"object","nullable":true,"description":"Cached Google Place Details (service-managed, read-only)."},"google_place_context":{"type":"string","nullable":true,"description":"Curated business summary injected into the agent prompt (service-managed, read-only)."},"google_place_fetched_at":{"type":"string","format":"date-time","nullable":true,"description":"Timestamp of the last successful Google fetch (drives the 28-day refresh)."},"knowledgebase_links":{"type":"array","items":{"type":"string"},"description":"URLs the agent references for knowledge"},"email":{"type":"array","items":{"type":"string"},"description":"Email addresses for notifications"},"status":{"type":"string","enum":["active","inactive","cancelled","pending"],"description":"Agent activation status"},"is_active":{"type":"boolean","description":"Whether the agent is currently active"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"NotetakerConfig":{"type":"object","additionalProperties":false,"description":"TLZ-597. Settings used ONLY when `agentType` is `notetaker`; ignored by the worker on any other type. Validated by `notetakerConfigSchema`, which is strict: an unknown property is a 400, not a silently stored key.\nCreating or updating a Notetaker also FORCES a set of columns regardless of what the request contains — `enableRecording` true, and `transferEnabled`, `webTransferEnabled`, `warmTransferEnabled`, `callDetection`, `echoDetection`, `detectAutomatedCallers`, `emailToolEnabled` and `teammateEnabled` false — and strips every model-invocable tool from `extensions`, keeping only platform-fired lifecycle webhooks (`before_call`, `after_call`, `on_error`, `on_disconnect`). `systemPrompt` is ignored: the canonical prompt is read from the database. These rules are applied from the agent's STORED type, so they also hold for a PUT that does not mention `agentType`.\nA create or type change returns 503 when the canonical prompt row has not been seeded (see the TLZ-597 migration), not 500 — the request is valid and can be retried.\n","properties":{"wake_word":{"type":"string","maxLength":64,"default":"Scribe","description":"The word that addresses the agent. Rejected if it is an everyday meeting word or one of the command words, which would make the commands unreachable.\n"},"disclosure_mode":{"type":"string","enum":["announce","silent"],"default":"announce","description":"`announce` speaks the join disclosure (the agent's `greetingMessage`). `silent` suppresses it, which is only lawful where all-party consent has been obtained separately — CA, FL, IL, MA, PA and WA require it.\n"},"conference_join_method":{"type":"string","enum":["dial_in_did","sip_uri","invite_to_bridge"]},"redaction_profile":{"type":"string","enum":["none","pii","pci","phi"],"default":"none","description":"Anything other than `none` arms transcript redaction, the same switch as `config.redact_phi`, and is therefore refused with 403 when the `phi_redaction` feature is locked for the organization.\n"},"summary_delivery":{"type":"object","additionalProperties":false,"properties":{"webhook_url":{"type":"string","format":"uri","description":"HTTPS, on a public host. Private, loopback, link-local and carrier-grade-NAT addresses and reserved internal names are refused: the whole meeting summary is posted here.\n"},"recap_channel":{"type":"string","enum":["email","sms"],"description":"Required whenever `recap_recipients` is non-empty."},"recap_recipients":{"type":"array","maxItems":25,"items":{"type":"string"},"description":"Email addresses when `recap_channel` is `email`, E.164 numbers when it is `sms`.\n"}}},"participant_roster":{"type":"array","maxItems":500,"items":{"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"string","maxLength":200},"role":{"type":"string","maxLength":200},"email":{"type":"string","format":"email"},"phone":{"type":"string","description":"E.164"}}}},"custom_pause_phrases":{"type":"array","maxItems":5,"default":[],"items":{"type":"string","maxLength":60},"description":"Extra phrases that pause the transcript, IN ADDITION to the built-in ones (\"off the record\", \"don't record this\", \"stop recording\", \"I don't consent\", ...), which always work. Matched anywhere in a sentence. Each phrase is 2 to 6 words of letters, apostrophes and spaces; stored lowercased, with whitespace collapsed and duplicates removed.\n"},"custom_resume_phrases":{"type":"array","maxItems":3,"default":[],"items":{"type":"string","maxLength":60},"description":"Extra phrases that resume the transcript, IN ADDITION to the built-in \"back on the record\". Must be said on their own (optionally after the wake word). Same word rules as `custom_pause_phrases`; a resume phrase may not contain \"off the record\" or equal a custom pause phrase. Use common English words: the offline listener used while paused ignores words outside its vocabulary.\n"}}},"VoiceSettings":{"type":"object","description":"Voice configuration for text-to-speech. Supported providers: Cartesia, Minimax.\n- Cartesia: Uses UUID format voice_id\n- Minimax: Requires voice_id (model name), speed, and model fields\n","properties":{"voice_id":{"type":"string","description":"Voice identifier. Format depends on ttsModel provider:\n- Cartesia: UUID (e.g., '79a125e8-cd45-4c13-8a67-188112f4dd22' for British Lady)\n- Minimax: Model name (e.g., 'English_radiant_girl', 'Lively_Girl', 'Calm_Woman', 'Casual_Guy')\n"},"speed":{"type":"number","description":"Speech speed multiplier (Minimax only, default 1.0)","example":1},"model":{"type":"string","description":"Voice model (Minimax only, e.g., 'speech-2.6-turbo')"},"language_voices":{"type":"object","description":"Optional. `es` is the voice the agent switches to when the caller speaks Spanish\n(set the agent language to `multi`; not supported with sttModel `cartesia`).\nFields: tts_model (the voice's provider), voice_id, and optional model, speed,\nlanguage (e.g. 'es'). Omit language_voices to remove it.\n","example":{"es":{"tts_model":"cartesia","voice_id":"<spanish voice id>","language":"es"}}}},"example":{"voice_id":"79a125e8-cd45-4c13-8a67-188112f4dd22"}},"AgentConfig":{"type":"object","description":"General agent configuration for audio and ambient settings","properties":{"ambient_type":{"type":"string","description":"Background ambience type","example":"office_ambience"},"ambient_volume":{"type":"number","description":"Background ambience volume (0.0-1.0)","example":0.3},"thinking_sounds_enabled":{"type":"boolean","description":"Enable thinking sounds during processing","example":true},"thinking_volume":{"type":"number","description":"Thinking sounds volume (0.0-1.0)","example":0.5},"background_audio_enabled":{"type":"boolean","description":"Enable background audio during calls","example":true},"confirm_transfers":{"type":"boolean","description":"Validate that a transfer destination can take the call before bridging. When the inbound call arrived over registered SIP and the destination is a registered extension (NetSapiens), availability is checked via BLF; all other transfers are attended-dialed and must press 1 to accept. If the destination is unavailable the caller stays with the agent.","example":false},"transfer_ring_timeout_seconds":{"type":"integer","minimum":10,"maximum":120,"description":"Seconds a transfer destination rings before the agent stops trying (default 25). Applies to warm transfers, confirm_transfers keypress checks and web widget transfers. A normal transfer is handed to the phone system and rings for as long as its no-answer setting allows.","example":25},"dtmf_transfer_enabled":{"type":"boolean","description":"Let the caller reach a transfer destination by keying it, so a rule that transfers to pbx:100 also fires when the caller presses 100 — no change to the rule needed. Applies to call transfer and warm transfer rules, and does nothing for an agent with neither. This does NOT control whether the agent listens for keypresses at all: that stays driven by the agent's own instructions mentioning DTMF, the keypad, or pressing keys.","example":false},"transfer_announcement_enabled":{"type":"boolean","description":"Set to false to silence the line the platform speaks right before a transfer (\"I am now transferring your call. Please stay on the line while I connect you.\"). Defaults to true. When off, nothing is spoken after the transfer starts, so the agent's prompt should announce the transfer itself.","example":true},"transfer_announcement":{"type":"string","maxLength":97,"description":"Replaces the platform's spoken transfer line with your own short sentence. Blank or omitted keeps the default line.","example":"Of course, transferring you to our customer service team now."},"response_filler_enabled":{"type":"boolean","default":false,"description":"When the caller is left waiting while the agent looks something up (a tool call), speak one short filler line after a delay. At most once per caller turn; never during transfers, ending the call or a knowledge-base lookup (which has its own line). A slow reply with no tool gets no filler.","example":true},"response_filler_delay_seconds":{"type":"number","minimum":0.5,"maximum":10,"default":1.5,"description":"Seconds of silence before the filler line is spoken.","example":1.5},"response_filler_phrases":{"type":"array","maxItems":10,"items":{"type":"string","maxLength":200},"description":"Filler lines; one is picked at random each time. Empty or omitted uses \"One moment, let me check that for you.\"","example":["One moment, let me check that for you.","Just a second while I look that up."]}},"example":{"ambient_type":"office_ambience","ambient_volume":0.3,"thinking_sounds_enabled":true,"thinking_volume":0.5,"background_audio_enabled":true,"confirm_transfers":false,"response_filler_enabled":true,"response_filler_delay_seconds":1.5,"dtmf_transfer_enabled":false}},"Extensions":{"type":"object","description":"Extensions configuration for webhooks, MCP servers, tools, and parameters. Under Feature Administration, a request that ADDS a gated function (the `ns_*` functions, `ns_reset_password`, `sage100_*`, `send_hosted_sms` or `check_hosted_sms_replies` (Hosted SMS), or a `listings_search` tool) is refused with 403 when that feature is locked for the organization. A `listings_search` tool must carry `parameters` `listings_url` and `attested_by` (the email of the person attesting that the business owns or may display the page; recorded as self-declared by the API caller). `attested_at` is always set from the server clock; a value sent is ignored. An AppFolio listings page (https://<company>.appfolio.com/listings) needs nothing else, and neither does any other public https listings page: once attested, its listings are read automatically after the save. The URL is stored canonical and `adapter` is derived from it (an `adapter` that does not match the address is refused with 400). The other keys default (`refresh_minutes` 10; `fetch_detail_pages` true for AppFolio, always false for any other page). `profile`, if sent, must be \"rentals\", and `fetch_detail_pages` must be \"true\" or \"false\". `refresh_minutes` must be 10, 15, 30 or 60. `business_name` is optional, at most 100 characters, and is the sender name shown in the texts. `sms_provider` is `twilio`, `netsapiens` or empty, and `sms_sender_extension` is 2 to 10 digits or empty. `enabled: false` is refused (remove the tool instead), an agent can have only one `listings_search` tool, and its `description` (the instructions) is at most 4,000 characters. Another tool named `search_listings` or `send_listing_link` is refused with 400.\n","properties":{"tools":{"type":"array","description":"Custom tools and integrations. TLZ-581: a `twilio_sms` tool must carry its own From number, as `parameters: [{\"key\": \"from_number\", \"value\": \"+1...\"}]`. Agent creation cannot set `integrations`, so there is no agent-level `fromNumber` to fall back to and a Send SMS tool without a From number is rejected with 400. On PUT the agent's stored `integrations.twilio.fromNumber` satisfies it instead.","items":{"$ref":"#/components/schemas/Tool"}},"webhooks":{"type":"array","description":"Webhook configurations for call events","items":{"$ref":"#/components/schemas/Webhook"}},"mcpServers":{"type":"array","description":"MCP (Model Context Protocol) server integrations","items":{"$ref":"#/components/schemas/McpServer"}},"parameters":{"type":"array","description":"Global parameters accessible to the agent","items":{"$ref":"#/components/schemas/Parameter"}}}},"Webhook":{"type":"object","description":"Webhook configuration for call lifecycle events","required":["url","condition"],"properties":{"name":{"type":"string","description":"Internal webhook identifier","example":"CRM_Integration"},"friendlyName":{"type":"string","description":"User-friendly display name","example":"CRM Integration Webhook"},"description":{"type":"string","description":"What the webhook does","example":"Sends call data to CRM after call ends"},"url":{"type":"string","format":"uri","description":"Webhook endpoint URL","example":"https://api.example.com/webhook/call-ended"},"method":{"type":"string","enum":["GET","POST","PUT","PATCH","DELETE"],"description":"HTTP method","default":"POST"},"condition":{"type":"string","enum":["before_call","during_call","after_call","before_transfer","on_transfer_success","on_transfer_failure","on_transfer","on_error","on_disconnect"],"description":"When to trigger the webhook"},"headers":{"type":"array","description":"Custom HTTP headers","items":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"example":[{"key":"Authorization","value":"Bearer your-api-key"},{"key":"Content-Type","value":"application/json"}]},"parameters":{"type":"array","description":"Request body parameters","items":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"example":[{"key":"call_id","value":"{{call_id}}"},{"key":"transcript","value":"{{transcript}}"}]},"type":{"type":"string","description":"Always 'webhook' for webhook tools","default":"webhook"},"include_agent_name":{"type":"boolean","default":true,"description":"Accepted so agents round-trip through GET then PUT, but has no effect here - the worker only reads include_agent_name on `extensions.tools` entries. There, after-call and other event-triggered webhooks (not during_call or before_call) add an `agent_name` field to the request body, and false omits it."}},"example":{"name":"After_Call_CRM","friendlyName":"CRM Update After Call","description":"Updates CRM with call summary after call ends","url":"https://api.example.com/crm/update","method":"POST","condition":"after_call","headers":[{"key":"Authorization","value":"Bearer api-key-here"}],"parameters":[{"key":"call_summary","value":"{{call_summary}}"},{"key":"caller_number","value":"{{caller_number}}"}]}},"Tool":{"type":"object","description":"Custom tool configuration","properties":{"name":{"type":"string","description":"Tool identifier"},"friendlyName":{"type":"string","description":"Display name"},"description":{"type":"string","description":"What the tool does"},"type":{"type":"string","enum":["webhook","tool","notification","function","transfer_agent","end_of_call_action","sage100","listings_search","netsapiens","twilio_sms","hosted_sms","hosted_sms_replies","schedule","teammate","teams_voicemail","email_tool","transfer_teams_voicemail","teammate_notification"],"description":"Tool type. `payment` is not accepted here; manage payment tools with PUT /v1/agents/{agentId}/payments/tool."},"parameters":{"type":"array","description":"Request body parameters (key/value pairs - AI extracts values from conversation)","items":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}}},"include_agent_name":{"type":"boolean","default":true,"description":"After-call and other event-triggered webhooks (not during_call or before_call) add an `agent_name` field to the request body. Set false to omit it for APIs that reject unknown fields (e.g. Freshdesk)."},"enabled":{"type":"boolean","default":true,"description":"Set false to switch a webhook off without deleting it - it is never fired and never offered to the agent. Omit (or true) to keep it on."}}},"McpServer":{"type":"object","description":"MCP (Model Context Protocol) server integration","properties":{"name":{"type":"string","description":"Server name","example":"Knowledge Base"},"url":{"type":"string","format":"uri","description":"Server endpoint URL","example":"https://mcp.example.com/knowledge"},"headers":{"type":"array","description":"Custom headers for authentication","items":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}}},"allowed_tool_names":{"type":"array","description":"Tools the agent may use from this server. Only these tool definitions are\nsent to the model, and calls to any other tool are rejected. Omitted or\nempty means every tool the server publishes (the default). Tools the server\nadds later stay off while a list is set. On an OpenTable reservation agent's\nStayAPI server, only OpenTable tools are kept and the availability,\navailability-slots and menu tools are always included. Names must match\nthe server's tool names exactly: when the server is reachable, unknown\nnames are removed and reported in `warnings`, and a list in which no name\nmatches is rejected with 400.\n","items":{"type":"string"},"example":["search_tickets","create_ticket"]},"pre_answer_calls":{"type":"array","description":"Tools the worker calls itself while the phone is ringing, so their results\ncan reach the agent before the greeting (a customer lookup by caller ID, for\nexample); results that arrive later are used from the next reply. Each\n`tool_name` must be one the agent may use from this server (checked when the\nserver is reachable at save time) and may appear once. String values in `arguments` may contain\n{{caller_id}}, {{agent_id}}, {{direction}}, {{room_name}} or {{timestamp}},\nfilled in for each call; a call whose value is missing (a withheld caller\nID) is skipped. All calls run at once and the AI greeting waits for all of\nthem, so keep it to 1-2: with more, the greeting is more likely to start\nbefore the results arrive. Choose lookup tools only — they run on every call.\n","items":{"type":"object","required":["tool_name"],"properties":{"tool_name":{"type":"string"},"arguments":{"type":"object"},"input_schema":{"type":"object","description":"The tool's input schema, kept so the editor can redraw the form."}}},"example":[{"tool_name":"contacts_get-contacts","arguments":{"query":"{{caller_id}}"}}]}}},"Parameter":{"type":"object","description":"Configuration parameter","properties":{"name":{"type":"string","description":"Parameter name"},"type":{"type":"string","enum":["String","Number","Boolean","Object","Array","JSON"],"description":"Parameter data type"},"value":{"type":"string","description":"Parameter value"}}},"InterpreterConfig":{"type":"object","description":"TLZ-598 Translator agent settings, stored in `agents.interpreter_config` with these same snake_case keys. The two languages must be English with Spanish or English with French, in either order. Each script map has exactly the two chosen languages as keys. Scripts may use only the placeholders {{business_name}} and {{client_name}}, and a name is required when a script uses it. Text is stored trimmed. Validation errors come back at their own path under `interpreterConfig`, with the error code (pair, scripts_missing, script_empty, script_too_long, token_unknown, token_value_missing, name_too_long, register, speaker_identification, required, voice_provider) at the start of the message. `voice_provider` (contract #16: a translator must stay on Cartesia) is the one exception -- it is reported at `ttsModel`, not a path under `interpreterConfig`.\n","required":["party_a_language","party_b_language","opening_scripts","closing_scripts","register"],"properties":{"party_a_language":{"type":"string","enum":["en-US","es-US","fr-FR"],"description":"Greeted first. Also stored as the agent's `language`."},"party_b_language":{"type":"string","enum":["en-US","es-US","fr-FR"]},"opening_scripts":{"type":"object","additionalProperties":{"type":"string","maxLength":2000},"description":"Spoken word for word once when the call connects, Party A first."},"closing_scripts":{"type":"object","additionalProperties":{"type":"string","maxLength":2000},"description":"Spoken once after both parties say goodbye. The agent never hangs up."},"business_name":{"type":"string","maxLength":120},"client_name":{"type":"string","maxLength":120},"register":{"type":"string","enum":["verbatim","plain"],"description":"verbatim keeps the speaker's wording; plain renders the same meaning in plain words."},"speaker_identification":{"type":"string","enum":["language","voice","both"],"description":"How the saved transcript tells the two speakers apart. Optional; omitted means language (and language is stored as an omitted key). voice and both turn on speech-to-text diarization, which adds a small per-minute charge. Never changes what the interpreter says.\n"}},"example":{"party_a_language":"es-US","party_b_language":"en-US","opening_scripts":{"es-US":"Hola y bienvenido(a). Soy la intérprete de voz con inteligencia artificial para esta llamada.","en-US":"Hello. I am the AI interpreter for this call, provided by {{business_name}}."},"closing_scripts":{"es-US":"Gracias por comunicarse con {{client_name}}.","en-US":"The interpretation session is now complete."},"business_name":"Acme Interpreting","client_name":"Example Co.","register":"verbatim"}},"CreateAgentRequest":{"type":"object","required":["name","organizationId"],"properties":{"name":{"type":"string","minLength":1,"maxLength":255,"description":"Display name of the agent"},"description":{"type":"string","description":"Description of what the agent does"},"greetingMessage":{"type":"string","description":"Initial message the agent says when call starts"},"outboundGreetingMessage":{"type":"string","description":"Greeting spoken when this agent places an outbound call"},"outboundStaticGreeting":{"type":"string","description":"Static line spoken verbatim before the outbound greeting (warnings/advisories, not alterable by AI)"},"aiDisclosure":{"type":"object","nullable":true,"description":"TLZ-477 US-4.3 AI-agent disclosure — the line telling the callee they are speaking to an AI. Null (or omitted) means none is configured. Spoken by the outbound worker, which records the outcome on the call as `disclosure_delivered` and `disclosure_timestamp` and echoes both in the result webhook.\n","required":["enabled"],"properties":{"enabled":{"type":"boolean","description":"Required and explicit — a compliance control is never defaulted."},"text":{"type":"string","maxLength":1000,"description":"The line spoken to the callee. Required when `enabled` is true.","example":"Just so you know, I'm an AI assistant calling on behalf of Acme."},"position":{"type":"string","enum":["after_greeting"],"default":"after_greeting","description":"Where the line lands in the call. Only `after_greeting` is implemented; other values are rejected rather than silently ignored."},"required_states":{"type":"array","items":{"type":"string","example":"CA"},"description":"US state/territory codes where disclosure is legally mandatory. Accepted case-insensitively, stored upper-case and de-duplicated. NOTE: stored as configuration only — nothing in the API evaluates it, since matching a callee to a state requires an area-code-to-state map that does not exist yet.\n"}}},"contactPolicy":{"type":"object","nullable":true,"description":"TLZ-477 compliance policy for this agent. Null (or omitted) means no policy: nothing is required and nothing is rejected. Unknown keys are rejected, so a typo is a 400 rather than a control that silently does nothing. Distinct from a retry policy's `contact_policy`, which holds per-number attempt caps.\n","properties":{"require_consent_basis":{"type":"boolean","description":"When true, Trigger Outbound Call for this agent must supply a `consent_basis` or it is rejected with 400."},"abandon_rate_alert_threshold":{"type":"number","minimum":0,"maximum":1,"default":0.03,"description":"TLZ-477 US-4.4 — the abandon rate above which this agent is flagged as breaching in GET /v1/metrics. Defaults to the FCC 3% limit when absent. An explicit 0 is meaningful (flag any abandoned call) and is not treated as unset.\n"},"calling_windows":{"type":"object","description":"TLZ-477 US-4.2 calling hours, evaluated on the CALLEE's clock. Absent means no hours restriction. Windows cannot wrap midnight, and `end_local` is exclusive, so \"21:00\" means \"before 9pm\".\n","properties":{"start_local":{"type":"string","example":"08:00","description":"Inclusive opening, 24-hour HH:MM, callee-local"},"end_local":{"type":"string","example":"21:00","description":"Exclusive close, 24-hour HH:MM, callee-local"},"timezone_source":{"type":"string","enum":["callee_npanxx","agent_timezone","explicit"],"description":"Which clock to read. `callee_npanxx` infers it from the destination area code (area-code granularity, so area codes spanning two zones resolve to the dominant one); `agent_timezone` uses the agent's own timezone; `explicit` uses the `timezone` field, which is then required.\n"},"timezone":{"type":"string","example":"America/Denver","description":"IANA zone; required when timezone_source is explicit"}}}}},"amdConfig":{"oneOf":[{"type":"boolean"},{"type":"object"}],"description":"Answering Machine Detection for outbound calls. Pass `true` to enable with defaults, `false` to disable, or an object to tune it. Unrelated to `callDetection` (mobile-number lookup). Object keys: `enabled` (boolean), `sensitivity` (fast|balanced|precise), `max_detection_ms` (int, null = use preset), `on_machine` (leave_voicemail|hang_up|continue_as_human), `on_silence` (reprompt|hang_up|continue_as_human), `uncertain_prompt` (string), `voicemail_max_duration_ms` (int), `voicemail_beep_wait_ms` (int).\n","default":false},"ivrNavigation":{"oneOf":[{"type":"boolean"},{"type":"object"}],"description":"IVR menu navigation for outbound calls. Pass `true` to enable with defaults, `false` to disable, or an object to tune it. Object keys: `enabled` (boolean), `target_intent` (reach_human|reach_department|custom), `target_department` (string), `spoken_keywords` (string[]), `max_attempts` (int), `max_duration_ms` (int), `fallback_action` (press_zero|hang_up|wait_for_human), `hold_max_wait_ms` (int). `dtmf_transport` is NOT accepted and returns 400: DTMF transport (rfc2833 vs SIP INFO) is a trunk-level setting that LiveKit's publish_dtmf does not expose, so accepting it would silently do nothing. Requires AMD: navigation is started by the answering-machine verdict, so it can never run with AMD off. Enabling it while AMD is off returns 400.\n","default":false},"voicemailMessage":{"type":"string","description":"Message spoken when the agent reaches an answering machine"},"voicemailMessageAudioUrl":{"type":"string","description":"Pre-recorded voicemail audio; played instead of voicemailMessage when set"},"aiGreetingPrompt":{"type":"string","description":"Prompt used to generate a dynamic AI greeting"},"systemPrompt":{"type":"string","description":"System prompt defining agent behavior"},"notetakerConfig":{"$ref":"#/components/schemas/NotetakerConfig"},"agentType":{"type":"string","enum":["simple","advanced","custom","appointment_scheduler","receptionist","message_taker","blank_slate","outbound","opentable_reservation","translator","notetaker"],"default":"simple","description":"The template this agent is created from. Under Feature Administration, `receptionist`, `appointment_scheduler`, `message_taker`, `blank_slate`, `opentable_reservation`, `notetaker` and `outbound` are refused with 403 when the matching Template is locked for the organization. `simple`, `advanced` and `custom` are never gated: `simple` is what the Support Agent (CRM) templates store and is also the default when agentType is omitted, so it cannot be attributed to one template. `simple`, `advanced` and `custom` identify no single template and are never gated; `simple` is the default, so callers that never chose a template are unaffected. `translator` (TLZ-598) creates an AI interpreter: it requires `interpreterConfig`, is refused with 403 when the Translator template is locked, takes the Translator template's defaults for every field the request leaves out, and stores Party A as the agent's `language`. A translator must use `ttsModel: cartesia`; any other vendor is refused with 400 (`voice_provider`).\n"},"interpreterConfig":{"allOf":[{"$ref":"#/components/schemas/InterpreterConfig"}],"nullable":true,"description":"Required when agentType is `translator`. Validated whenever present, on any agent type.\n"},"model":{"type":"string","description":"LLM model (e.g., gpt-4.1, gpt-4o)","default":"gpt-4.1"},"voiceModel":{"type":"string","description":"Voice/speech model","default":"gpt-4o-realtime-preview"},"sttModel":{"type":"string","description":"Speech-to-text model","default":"deepgram"},"ttsModel":{"type":"string","description":"Text-to-speech provider (cartesia, minimax)","default":"cartesia"},"voiceSettings":{"$ref":"#/components/schemas/VoiceSettings"},"config":{"$ref":"#/components/schemas/AgentConfig"},"extensions":{"$ref":"#/components/schemas/Extensions"},"transferEnabled":{"type":"boolean","description":"Enable SIP call transfer","default":false},"webTransferEnabled":{"type":"boolean","description":"Enable web-call transfer. Only takes effect when transferEnabled is true; otherwise stored as false.","default":true},"teammateEnabled":{"type":"boolean","description":"Enable Teams teammate integration","default":false},"teammateVoicemailTargets":{"type":"array","description":"Teams users whose voicemail the agent can reach","items":{"type":"object","required":["name","upn"],"properties":{"name":{"type":"string"},"upn":{"type":"string","description":"User principal name (user@domain)"}}}},"emailServerId":{"type":"string","format":"uuid","nullable":true,"description":"Email server for the email tool; must belong to the account (see GET /v1/email-servers)"},"pronunciations":{"type":"array","description":"Custom word pronunciations. Requires a TTS voice that supports them; MiniMax voices are English-only for pronunciations.","items":{"type":"object"}},"emailToolEnabled":{"type":"boolean","description":"Enable email sending during calls","default":false},"emailToolInstructions":{"type":"string","description":"Instructions for when/how to send emails"},"callDetection":{"type":"boolean","description":"Enable mobile phone detection","default":false},"echoDetection":{"type":"boolean","description":"Enable echo detection during calls","default":false},"detectAutomatedCallers":{"type":"boolean","description":"Enable AI-based automated caller detection during calls","default":false},"enableRecording":{"type":"boolean","description":"Enable call recording","default":false},"sendRecordingLinkWithSummary":{"type":"boolean","description":"Include a link to the call recording in the post-call summary notification. Requires call recording to be enabled to have any effect.","default":false},"language":{"type":"string","description":"Agent language: en-US, es-US, or multi (multilingual)","default":"en-US"},"timezone":{"type":"string","description":"Agent timezone in IANA format (e.g., America/New_York)","default":"America/New_York"},"transferRules":{"type":"string","description":"Rules and conditions for call transfers"},"warmTransferEnabled":{"type":"boolean","description":"Enable warm transfer routing for this agent","default":false},"warmTransferRules":{"type":"string","description":"Rules and instructions for warm transfers"},"warmTransferHandoffInstructions":{"type":"string","description":"What the AI says to the human agent during a warm transfer consult leg. Blank uses the built-in briefing."},"warmTransferMohUrl":{"type":"string","format":"uri","description":"Public URL of the hold-music file played to the caller during warm transfer. Obtain one by uploading a file via POST /v1/agents/{agentId}/moh, or set directly if you already host the audio. Leave empty for built-in hold music."},"warmTransferPassThroughCallerNumber":{"type":"boolean","description":"Whether to pass the original caller's number through to the transfer destination. Default is true."},"knowledgebaseLinks":{"type":"array","items":{"type":"string"},"description":"Array of URLs the agent references for knowledge. This is a legacy\nfield and is NOT the RAG Knowledge Base — for vector search use\nragEnabled and knowledgeBaseId below.\n"},"ragEnabled":{"type":"boolean","description":"Enables live vector search (RAG) against the assigned Knowledge Base.\nRequires knowledgeBaseId — an agent with no Knowledge Base does not\nretrieve regardless of this flag. Returns 403 forbidden when Knowledge\nBase is not enabled for the target organization.\n"},"knowledgeBaseId":{"type":"string","format":"uuid","nullable":true,"description":"Organization-level Knowledge Base the agent retrieves from. Must belong\nto the same organization the agent is created in, otherwise the request\nis rejected. List available ids via\nGET /v1/organizations/{organization_id}/knowledge-bases.\n"},"ragConfig":{"type":"object","description":"RAG retrieval tuning. Unknown keys are rejected.","properties":{"top_k":{"type":"integer","minimum":1,"maximum":50,"description":"Number of chunks to retrieve per query."},"rewrite_query":{"type":"boolean","description":"Rewrite the caller's phrasing into a search query before retrieving."},"max_context_tokens":{"type":"integer","minimum":1,"maximum":200000,"description":"Cap on retrieved context injected into the prompt."},"fallback_message":{"type":"string","maxLength":2000,"description":"What the agent says when retrieval returns nothing useful."}}},"googlePlaceId":{"type":"string","description":"Google Place ID linking the agent to a Google Business Profile listing\n(TLZ-426). The platform fetches the listing's details (hours, location,\nphone, website, services, ratings) and the agent uses them as verified\nfacts on calls.\n","example":"ChIJVS6XxJPFU4gRoE8c8ql2lCQ"},"email":{"type":"array","items":{"type":"string"},"description":"Email addresses for notifications and email tool"},"status":{"type":"string","enum":["active","inactive","cancelled","pending"],"default":"active","description":"Agent activation status. Defaults to active when omitted."},"organizationId":{"type":"string","format":"uuid","description":"Organization to create the agent in"}},"example":{"name":"Customer Support Agent","description":"Handles customer inquiries and support tickets","greetingMessage":"Hello! Thank you for calling. How can I help you today?","outboundGreetingMessage":"Hi, this is Ava calling from Acme Corp.","systemPrompt":"You are a helpful customer support agent. Be friendly, professional, and resolve issues efficiently.","agentType":"simple","model":"gpt-4.1","voiceModel":"gpt-4o-realtime-preview","sttModel":"deepgram","ttsModel":"cartesia","voiceSettings":{"voice_id":"79a125e8-cd45-4c13-8a67-188112f4dd22","speed":1},"config":{"ambient_type":"office_ambience","ambient_volume":0.3,"thinking_sounds_enabled":true,"thinking_volume":0.5,"confirm_transfers":false},"extensions":{"tools":[{"name":"search_listings","friendlyName":"Listings_Search","type":"listings_search","condition":"during_call","parameters":[{"key":"listings_url","value":"https://samplepm.managebuilding.com/Resident/public/rentals"},{"key":"adapter","value":"generic"},{"key":"profile","value":"rentals"},{"key":"refresh_minutes","value":"10"},{"key":"fetch_detail_pages","value":"false"},{"key":"attested_by","value":"ops@acmeproperties.com"},{"key":"attested_at","value":""},{"key":"sms_provider","value":""},{"key":"sms_sender_extension","value":""},{"key":"business_name","value":"Acme Properties"}]}],"webhooks":[{"name":"After_Call_Webhook","friendlyName":"Post-Call CRM Update","url":"https://api.example.com/webhook/call-complete","method":"POST","condition":"after_call","headers":[{"key":"Authorization","value":"Bearer your-api-key"}],"parameters":[{"key":"transcript","value":"{{transcript}}"},{"key":"call_duration","value":"{{call_duration}}"}]}],"parameters":[{"name":"company_name","type":"String","value":"Acme Corp"}]},"transferEnabled":false,"emailToolEnabled":true,"emailToolInstructions":"Send email confirmation when customer requests it","organizationId":"123e4567-e89b-12d3-a456-426614174000"}},"Pagination":{"type":"object","properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"totalPages":{"type":"integer"},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TranscriptShare":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Share identifier (the last path segment of `url`)"},"url":{"type":"string","format":"uri","description":"Public transcript URL. Uses the account's active custom domain when one is configured."},"call_log_id":{"type":"string","format":"uuid"},"agent_id":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time"}}},"Guardrail":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organization_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"enabled":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"MobileCheckResult":{"type":"object","properties":{"success":{"type":"boolean"},"valid":{"type":"boolean","description":"Whether the number is a valid phone number"},"isMobile":{"type":"boolean"},"lineType":{"type":"string","description":"Line type (e.g. mobile, landline, unknown)"},"number":{"type":"string"},"internationalFormat":{"type":"string"},"countryCode":{"type":"string"},"countryName":{"type":"string"},"carrier":{"type":"string"},"location":{"type":"string"}}},"VoiceOption":{"type":"object","properties":{"id":{"type":"string","description":"Voice ID (use as voiceSettings.voice_id on agents)"},"name":{"type":"string"},"description":{"type":"string"},"provider":{"type":"string","enum":["cartesia","minimax","inworld"]},"previewUrl":{"type":"string","description":"Sample audio URL, when available"},"model":{"type":"string","description":"TTS model the voice runs on, when provider-specific"},"language":{"type":"string","description":"ISO language code the voice speaks"},"custom":{"type":"boolean","description":"True for organization-scoped cloned voices"}}}}},"security":[{"BearerAuth":[]}],"paths":{"/oauth/token":{"post":{"summary":"Request Access Token","description":"Accepts a JSON request body for the Client Credentials Grant and translates it into the `application/x-www-form-urlencoded` format required by Auth0. Returns an access token.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"client_id":{"type":"string","description":"Auth0 client ID"},"client_secret":{"type":"string","description":"Auth0 client secret"},"audience":{"type":"string","description":"API audience"},"grant_type":{"type":"string","enum":["client_credentials"],"description":"Must be 'client_credentials'"},"client_assertion":{"type":"string","description":"(Optional) Private Key JWT assertion"},"client_assertion_type":{"type":"string","description":"(Optional) Assertion type for Private Key JWT"}}},"example":{"client_id":"YOUR_CLIENT_ID","client_secret":"YOUR_CLIENT_SECRET","audience":"https://api.telzino.com/","grant_type":"client_credentials"}}}},"responses":{"200":{"description":"Access token response","content":{"application/json":{"schema":{"type":"object","properties":{"access_token":{"type":"string"},"token_type":{"type":"string"},"expires_in":{"type":"integer"}}}}}},"400":{"description":"Invalid request"},"500":{"description":"Server error"}}}},"/v1/accounts/{accountId}/resellers":{"get":{"summary":"Get resellers for an account","parameters":[{"in":"path","name":"accountId","required":true,"schema":{"type":"string"}},{"in":"query","name":"external_id","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"List of resellers. Each record also carries `organization_count` and\n`netsapiens_platforms`. An account may own resellers on more than one\nNetSapiens platform (for example a production platform and a sandbox\none), and the same `external_id` can legitimately appear once per\nplatform — use `netsapiens_platforms[].id`/`name` to tell those apart\n(`platform_type` is the vendor, `netsapiens` or `skyswitch`, not the\nenvironment), and `organization_count` to identify the record that owns\ntenants. An empty `netsapiens_platforms` means the record is linked to no\nplatform at all; where a record with the same `external_id` does have one,\nthe empty record is a legacy artefact and can be disregarded. Both fields\nare omitted if the lookup that populates them fails.\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"account_id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true,"description":"Display name of the reseller"},"external_id":{"type":"string","nullable":true,"description":"Slug identifying the reseller in the PBX"},"created_at":{"type":"string","format":"date-time"},"organization_count":{"type":"integer","description":"Number of organizations owned by this reseller record."},"netsapiens_platforms":{"type":"array","description":"NetSapiens platforms this reseller is linked to.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"platform_type":{"type":"string"}}}}}}}}}}}},"400":{"description":"Invalid accountId (must be a UUID)"},"401":{"description":"Unauthorized. Also `invalid_token` when the token was minted before the API key's secret was last rotated; request a new token."},"403":{"description":"`insufficient_scope` — the API key is limited to a single reseller or organization. This endpoint lists every reseller of the account, so it is open to account-wide keys only."},"404":{"description":"Account not found"},"500":{"description":"Internal server error"},"503":{"description":"`temporarily_unavailable` — the API key's scope could not be verified. Retry shortly."}}},"post":{"summary":"Create a new reseller for an account","parameters":[{"in":"path","name":"accountId","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"resellerName":{"type":"string"},"external_id":{"type":"string"}}},"example":{"resellerName":"Test Reseller","external_id":"external-reseller-123"}}}},"responses":{"200":{"description":"A reseller with this `external_id` already exists in the account; the\nexisting record is returned and nothing was created. Creation is\nidempotent on `external_id` so a retried call cannot mint a duplicate.\n"},"201":{"description":"Reseller created"},"400":{"description":"Bad request (invalid accountId or request body)"},"401":{"description":"Unauthorized (missing or invalid M2M credentials)"},"403":{"description":"`insufficient_scope` — the API key is limited to a single reseller or organization and cannot create resellers."},"404":{"description":"Account not found, or not the caller's own account"},"500":{"description":"Internal server error"}}}},"/v1/agents/{agentId}/call-logs":{"get":{"tags":["Call Logs"],"summary":"List call logs for an agent","description":"Returns a paginated list of call transcripts/logs for a specific agent.","parameters":[{"in":"path","name":"agentId","required":true,"schema":{"type":"string","format":"uuid"},"description":"The agent ID"},{"in":"query","name":"page","schema":{"type":"integer","default":1},"description":"Page number for pagination"},{"in":"query","name":"limit","schema":{"type":"integer","default":50,"maximum":100},"description":"Number of items per page (max 100)"},{"in":"query","name":"startDate","schema":{"type":"string","format":"date-time"},"description":"Only calls started at or after this time (ISO 8601)"},{"in":"query","name":"endDate","schema":{"type":"string","format":"date-time"},"description":"Only calls started at or before this time (ISO 8601)"}],"responses":{"200":{"description":"List of call logs with pagination info","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CallLog"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"401":{"description":"Unauthorized - missing or invalid token"},"403":{"description":"Forbidden - no access to this agent"},"404":{"description":"Agent not found"},"500":{"description":"Internal server error"}}}},"/v1/agents/{agentId}/history/{versionId}/revert":{"post":{"tags":["Agents"],"summary":"Revert an agent to an earlier version","description":"Saves the agent with the settings recorded in an earlier version from\n`GET /v1/agents/{agentId}/history`. Nothing is removed from the history: the revert is\nrecorded as a new version (`action: reverted`, `source: dashboard-api`), which appears\nin the history a few seconds later.\n\nOnly the recorded settings are restored. Functions, outbound and voicemail greetings,\nAMD/IVR, screenshare, email server, call limits and retries, agent type, agent status,\nintegrations, SIP and knowledge-base documents stay as they are now. Web transfer also\nstays, except that restoring call transfer off turns web transfer off with it.\n\nThe organization's feature locks apply, with the same messages as\n`PUT /v1/agents/{agentId}`: restoring an AI model, voice provider, cloned voice, warm\ntransfer, knowledge base (RAG), PII redaction or email template the organization can\nno longer use is refused with 403, as is changing the knowledge base settings while RAG\nis no longer available. Keeping a setting the agent already has on is not refused.\n\nReverting to a version whose settings match the current ones changes nothing, and no\nnew version is recorded.\n\nNo request body.\n","parameters":[{"in":"path","name":"agentId","required":true,"schema":{"type":"string","format":"uuid"},"description":"The unique identifier of the agent"},{"in":"path","name":"versionId","required":true,"schema":{"type":"integer","minimum":1},"description":"The `id` of a version from GET /v1/agents/{agentId}/history"}],"responses":{"200":{"description":"The agent was reverted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"message":{"type":"string"}}},"example":{"success":true,"message":"Agent reverted. If any settings changed, the new version appears in the change history within a few seconds."}}}},"400":{"description":"versionId is not a positive integer"},"401":{"description":"Unauthorized - missing or invalid token"},"403":{"description":"Forbidden - not a LiveKit agent, a setting in that version is no longer available to the organization, or the version breaks one of the agent's save rules (the reason is in error_description)"},"404":{"description":"Agent not found or access denied, or no such version for this agent"},"500":{"description":"Internal server error"},"503":{"description":"The revert could not be done right now; retry shortly"}}}},"/v1/agents/{agentId}/history":{"get":{"tags":["Agents"],"summary":"List an agent's change history","description":"Returns the agent's recorded settings changes, newest first: who made each change,\nwhen, from where, and every field that changed with its old and new value. This is\nthe same record the Dashboard's Change History tab and Connect's Change history page\nshow.\n\nOnly the agent's main settings are recorded. Not recorded: agent status (active /\ninactive), functions, outbound and voicemail greetings, AMD/IVR, web transfer,\nscreenshare, email server, call limits and retries, agent type, integrations and\ncredentials, SIP, and knowledge-base documents. Only LiveKit agents are recorded; any other agent returns an empty list.\n\nA change made through the API appears here a few seconds after it is saved.\n\nUse an entry's `id` with `POST /v1/agents/{agentId}/history/{versionId}/revert`.\n`version` is a display number (1 = oldest on record) and can shift when old\nversions are removed by the account's retention setting; `id` never changes.\n","parameters":[{"in":"path","name":"agentId","required":true,"schema":{"type":"string","format":"uuid"},"description":"The unique identifier of the agent"},{"in":"query","name":"page","schema":{"type":"integer","default":1,"minimum":1},"description":"Page number (1-indexed). A value that is not a whole number of 1 or more falls back to 1; a page past the end returns an empty list."},{"in":"query","name":"limit","schema":{"type":"integer","default":50,"minimum":1,"maximum":100},"description":"Number of versions per page (max 100). A value that is not a whole number of 1 or more falls back to 50; a value over 100 is capped at 100."}],"responses":{"200":{"description":"One page of versions, newest first","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer","description":"Permanent version id; use it to revert"},"version":{"type":"integer","description":"Display number, 1 = oldest on record"},"action":{"type":"string","enum":["created","updated","reverted","deleted"]},"reverted_from_id":{"type":"integer","nullable":true,"description":"For a revert, the id of the version it restored"},"created_at":{"type":"string","format":"date-time"},"actor":{"type":"object","properties":{"id":{"type":"string","description":"The user id, `api:<accountId>` for an API key, or `system`"},"email":{"type":"string","nullable":true}}},"source":{"type":"string","description":"Where the change came from, e.g. dashboard-ui, dashboard-api, connect-ui, revert, system"},"changes":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","description":"Field name; settings blobs are split per key, e.g. `config.dtmf_enabled`"},"before":{"nullable":true},"after":{"nullable":true}}}}}}},"pagination":{"type":"object","properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"totalPages":{"type":"integer"},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}}}}}}},"401":{"description":"Unauthorized - missing or invalid token"},"404":{"description":"Agent not found or access denied"},"500":{"description":"Internal server error"},"503":{"description":"The change history store is temporarily unavailable"}}}},"/v1/agents/{agentId}/moh":{"post":{"tags":["Agents"],"summary":"Upload warm-transfer hold music","description":"Uploads an MP3 or WAV file (max 4 MB) to be played to the caller while on\nhold during a warm transfer. The file is stored and its public URL is saved\nto the agent's `warm_transfer_moh_url`. Replacing an existing file removes the\nprevious one. Only livekit agents are supported.\n","parameters":[{"in":"path","name":"agentId","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary","description":"MP3 or WAV audio file, max 4 MB."}}}}}},"responses":{"200":{"description":"Upload succeeded","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"url":{"type":"string","format":"uri"}}}}}}}},"400":{"description":"No file provided","wrong type":null,"too large":null,"or invalid content":null},"401":{"description":"Unauthorized"},"403":{"description":"Not a livekit agent"},"404":{"description":"Agent not found or access denied"},"500":{"description":"Internal server error"}}},"delete":{"tags":["Agents"],"summary":"Remove warm-transfer hold music","description":"Deletes any uploaded hold-music file and clears the agent's `warm_transfer_moh_url`.","parameters":[{"in":"path","name":"agentId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Removed","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Not a livekit agent"},"404":{"description":"Agent not found or access denied"},"500":{"description":"Internal server error"}}}},"/v1/agents/{agentId}/outbound-calls/{callId}":{"get":{"tags":["Outbound Calls"],"summary":"Get an outbound call with current status and result","parameters":[{"in":"path","name":"agentId","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"path","name":"callId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Outbound call record","including summary_text from the linked transcript when available":null},"401":{"description":"Unauthorized - missing or invalid token"},"404":{"description":"Not found"},"500":{"description":"Internal server error"}}},"delete":{"tags":["Outbound Calls"],"summary":"Cancel a scheduled retry, or soft-delete a finished call","description":"Routes by call status. A retry still in the `scheduled` state is cancelled so it never dials (TLZ-476); one already promoted and dialing is left untouched. Any other call is soft-deleted: it stops appearing in the outbound-call and call-log endpoints and its recording and share link stop resolving, while aggregate usage and billing figures are unchanged. Only a scheduled retry or a finished call can be removed — one still in flight cannot.\n","parameters":[{"in":"path","name":"agentId","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"path","name":"callId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Scheduled retry cancelled","or finished call record deleted":null},"401":{"description":"Unauthorized - missing or invalid token"},"404":{"description":"Agent or call not found","or not accessible to this token":null},"409":{"description":"Call is no longer scheduled / still in flight and cannot be removed yet"},"500":{"description":"Internal error"}}}},"/v1/agents/{agentId}/outbound-calls":{"post":{"tags":["Outbound Calls"],"summary":"Trigger an outbound call","description":"Creates an outbound call record; the dispatcher service places the call. Requires a livekit agent.","parameters":[{"in":"path","name":"agentId","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["to_number"],"properties":{"to_number":{"type":"string","example":"+15105550123","description":"E.164 destination"},"call_context":{"type":"string","description":"Optional context appended to the agent's system prompt for this call"},"greeting":{"type":"string","description":"Exact opening line spoken when the callee answers (overrides agent outbound greeting and auto-generated opener)"},"voicemail_message":{"type":"string","description":"Message left if answering machine detection reaches a voicemail box. Overrides the agent's voicemail_message and suppresses its pre-recorded audio. Requires amd_config.enabled on the agent."},"amd_config":{"type":"object","description":"Per-call answering machine detection settings, merged over the agent's amd_config (per-call keys win). Same keys as the agent field: enabled, sensitivity, max_detection_ms, on_machine, on_silence, uncertain_prompt, voicemail_max_duration_ms, voicemail_beep_wait_ms."},"ivr_navigation":{"type":"object","description":"Per-call IVR menu navigation settings, merged over the agent's ivr_navigation (per-call keys win). Keys: enabled, target_intent (reach_human|reach_department|custom), target_department (max 200 chars), spoken_keywords (string[], max 20 x 100 chars), max_attempts (1-10), max_duration_ms (1-600000), fallback_action (press_zero|hang_up|wait_for_human), hold_max_wait_ms (1-1800000). Requires amd_config.enabled (agent or per-call): IVR navigation runs off the answering-machine verdict, so it can never start with AMD off. Sending this with navigation effectively enabled while AMD is effectively off returns 400 and places NO call, rather than dialling and ignoring the setting. Unknown keys (including dtmf_transport) are rejected with 400 unrecognized_keys."},"ivr_map":{"type":"array","description":"Optional per-call scripted IVR map applied for this call only, bypassing the LLM navigator. Ordered steps of { wait_for?: string, wait_ms?: number (max 60000), send_dtmf: string (1-32 chars of 0-9 * #) } — e.g. [{ wait_for: 'billing', send_dtmf: '2' }, { wait_ms: 3000, send_dtmf: '0' }]. Max 20 steps, and the wait_ms values must total at most 600000 (the worker abandons a longer script part-way). Sending a map switches navigation on for this call by itself (US-2.5: 'when present, LiveKit's LLM-based navigator is bypassed and Telzino drives DTMF directly'), so ivr_navigation.enabled is NOT also required. Still requires amd_config.enabled (agent or per-call): a map sent while AMD is effectively off returns 400 and places NO call."},"metadata":{"type":"object","description":"Opaque metadata echoed back in results and callback. One key is reserved (TLZ-477 US-4.5): `campaign_id`, a 1-200 character string used to group calls for the compliance audit export (GET /v1/outbound-calls/export?campaign_id=...). It is validated so the filter is trustworthy — a non-string or blank value is rejected rather than stored where it could never match. Every other key stays free-form.\n"},"callback_url":{"type":"string","format":"uri","description":"HTTPS URL to POST the call result to. TLZ-477 US-4.3 adds `disclosure_delivered` (boolean) and `disclosure_timestamp` (ISO 8601) to that payload: true means the agent's AI disclosure was spoken, false means one was due and did not play, and null means none was due for this call.\n"},"retry_policy_id":{"type":"string","format":"uuid","description":"TLZ-476 — retry policy to use for this call, overriding the agent's default_retry_policy_id for this call only. Must belong to the caller's account (else 400)."},"consent_basis":{"type":"string","enum":["express_written","ebr","informational_exempt","other"],"description":"TLZ-477 — the legal basis this call is placed under. Required only when the agent's contactPolicy.require_consent_basis is true, in which case omitting it returns 400 consent_required. Always recorded on the call when supplied."},"consent_reference":{"type":"string","maxLength":200,"description":"Free-text identifier for where that consent is recorded (a CRM id, a form submission id). Never validated; stored for the compliance audit export."},"callee_timezone":{"type":"string","example":"America/Denver","description":"TLZ-477 — the callee's IANA timezone, overriding the area-code lookup for this call. Use it when the destination's area code would mislead (ported numbers, callees who moved). Only consulted when the agent has a calling window configured."}}}}}},"responses":{"202":{"description":"Call queued","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string"},"room_name":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"next_attempt_at":{"type":"string","format":"date-time","nullable":true,"description":"Set when the call was queued for the next calling window (status 'scheduled') rather than dialled now; null otherwise."}}}}}},"400":{"description":"Invalid request (validation failed, agent is not a livekit agent, or retry_policy_id not found for this account). Also returned with error=consent_required when the agent requires a consent_basis and none was supplied."},"401":{"description":"Unauthorized - missing or invalid token"},"403":{"description":"Outbound calling is locked for the agent's organization (error=feature_locked)."},"404":{"description":"Agent not found or access denied"},"409":{"description":"Blocked by policy. `error: policy_violation` — the retry policy's per-number contact caps block another attempt to this number. `error: out_of_window` (TLZ-477 US-4.2) — the callee's local time is outside the agent's calling window and the retry policy has not set `reschedule_out_of_window`; with that flag set the call is accepted with 202 and status `scheduled` instead. `error: suppressed` (TLZ-477 US-4.6) — the number is on the organization's outbound suppression list and must not be dialled at all.\n"},"429":{"description":"Agent is at its concurrent-call limit (agent_at_capacity); retry later"},"500":{"description":"Failed to create call record"}}},"get":{"tags":["Outbound Calls"],"summary":"List outbound calls for an agent","parameters":[{"in":"path","name":"agentId","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"query","name":"page","schema":{"type":"integer","default":1}},{"in":"query","name":"limit","schema":{"type":"integer","default":50,"maximum":100}},{"in":"query","name":"status","schema":{"type":"string"},"description":"Filter by call status"}],"responses":{"200":{"description":"Paginated outbound call list"},"401":{"description":"Unauthorized - missing or invalid token"},"404":{"description":"Agent not found or access denied"},"500":{"description":"Internal server error"}}}},"/v1/agents/{agentId}/payments/readiness":{"get":{"summary":"Get an agent's payment readiness and current in-call payment config","description":"Reports whether an agent can be armed to take payments and, if the tool is enabled, its current guardrails. Use this to discover WHY enabling is blocked before calling `PUT /v1/agents/{agentId}/payments/tool` — e.g. the merchant isn't charge-ready yet, or the agent's organization has no usable Twilio connection (or, for a not-yet-migrated agent, no credentials of its own) or the agent has no From number. The platform fee is intentionally not exposed here.\n","parameters":[{"in":"path","name":"agentId","required":true,"schema":{"type":"string"}},{"in":"query","name":"refresh","schema":{"type":"boolean","default":false},"description":"Set to true to force a live re-sync of merchant status from Stripe for this call"}],"responses":{"200":{"description":"Agent payment readiness. `can_enable` accounts for every blocker: merchant charge-readiness, Twilio SMS, and `feature_locked` — true when Card Payments (Stripe) is locked for the organization in Feature Administration. Reading readiness is never blocked, and an already-armed tool can still be disabled.\n"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Agent not found"},"500":{"description":"Internal server error"},"502":{"description":"Could not verify Stripe status"}}}},"/v1/agents/{agentId}/payments/tool":{"put":{"summary":"Enable or disable the in-call payment tool on an agent","description":"Arms (or disarms) the agent's ability to take card payments during a call. Enabling writes the payment tool and its guardrails into the agent config that the voice worker reads at call time. Enabling is gated: the agent's organization must be charge-ready in Stripe (completed Connect onboarding) AND the agent's organization must have a usable Twilio connection and the agent a From number, because the payment link is delivered to the caller by text. Disabling only removes the tool and is always allowed.\n","parameters":[{"in":"path","name":"agentId","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["enabled"],"properties":{"enabled":{"type":"boolean"},"success_url":{"type":"string","description":"HTTPS URL the caller is redirected to after paying (used only when enabling)."},"max_single_charge_cents":{"type":"integer"},"max_per_call_cents":{"type":"integer"},"max_attempts_per_call":{"type":"integer"}}}}}},"responses":{"200":{"description":"Tool state updated"},"400":{"description":"Bad request / invalid guardrails"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — either the agent's organization is not in your account, or Card Payments (Stripe) is locked for it in Feature Administration. Only ARMING a tool that is currently off is blocked: `enabled: false` always succeeds, and re-sending `enabled: true` for an already-enabled tool succeeds too, so a locked organization can still adjust its guardrails or turn the tool off.\n"},"404":{"description":"Agent not found"},"409":{"description":"Not charge-ready or Twilio not configured"},"500":{"description":"Internal server error"},"502":{"description":"Could not verify Stripe status"}}}},"/v1/agents/{agentId}":{"get":{"tags":["Agents"],"summary":"Get a specific agent","description":"Retrieves details of a specific agent by ID. Only returns agents accessible by the authenticated account.","parameters":[{"in":"path","name":"agentId","required":true,"schema":{"type":"string","format":"uuid"},"description":"The unique identifier of the agent"}],"responses":{"200":{"description":"Agent details. Secret fields (integration passwords/tokens, SIP password) are never included.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Agent"}}}}}},"401":{"description":"Unauthorized - missing or invalid token"},"404":{"description":"Agent not found or access denied"},"500":{"description":"Internal server error"}}},"put":{"tags":["Agents"],"summary":"Update an agent","description":"Updates an existing agent.\n\nTop-level field names differ between read and write: GET returns the stored\nsnake_case columns (e.g. `system_prompt`), while this endpoint takes camelCase\n(e.g. `systemPrompt`). Keys inside nested objects such as `voiceSettings` and\n`config` stay snake_case. `email` is sent as an array of addresses but returned by\nGET as the stored string.\n","parameters":[{"in":"path","name":"agentId","required":true,"schema":{"type":"string","format":"uuid"},"description":"The unique identifier of the agent"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAgentRequest"},"example":{"name":"Updated Agent Name","systemPrompt":"Updated system prompt...","status":"active"}}}},"responses":{"200":{"description":"Agent updated successfully. Secret fields (integration passwords/tokens, SIP password) are never included in the response.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Agent"},"warnings":{"type":"array","items":{"type":"string"},"description":"Present only when configured MCP servers failed their connection autotest; the update is still applied."}}}}}},"400":{"description":"Invalid request data"},"401":{"description":"Unauthorized - missing or invalid token"},"403":{"description":"Forbidden - cannot update this agent, or the request enables a feature locked for its organization in Feature Administration: an `integrations` entry, `ragEnabled: true` (Knowledge Base), `warmTransferEnabled: true` (Warm Transfer), a newly selected cloned `voiceSettings.voice_id` (Custom Voices), a `googlePlaceId` DIFFERENT from the one already stored (Business Listing), or an `agentType` CHANGED to a locked template (Templates), or a gated function ADDED to `extensions.tools` (`ns_*`, `ns_reset_password`, `sage100_*`, `send_hosted_sms` / `check_hosted_sms_replies`, `listings_search`). A `listings_search` change that arms the tool on a locked organization gets this 403 even when the tool is also invalid. Only a genuine change is gated: re-sending the agent's current Place ID or agentType is allowed, so a read-modify-write round trip still works for a locked organization, and clearing a value — setting it to `null` — is never blocked.\n"},"404":{"description":"Agent not found or access denied"},"500":{"description":"Internal server error"}}},"delete":{"tags":["Agents"],"summary":"Delete an agent","description":"Deletes an existing agent.","parameters":[{"in":"path","name":"agentId","required":true,"schema":{"type":"string","format":"uuid"},"description":"The unique identifier of the agent"}],"responses":{"200":{"description":"Agent deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"message":{"type":"string"}}}}}},"401":{"description":"Unauthorized - missing or invalid token"},"403":{"description":"Forbidden - cannot delete this agent"},"404":{"description":"Agent not found or access denied"},"409":{"description":"The agent has outbound calls still in flight; deleting it would discard them"},"500":{"description":"Internal server error"}}}},"/v1/agents/{agentId}/sip/registration":{"put":{"tags":["Agents"],"summary":"Set SIP Registration for an agent","description":"Saves SIP registration data for a LiveKit agent.","parameters":[{"in":"path","name":"agentId","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["sipDomain","username","password"],"properties":{"sipDomain":{"type":"string"},"username":{"type":"string"},"password":{"type":"string"},"proxy":{"type":"string"},"port":{"type":"integer","nullable":true,"default":5060,"description":"Registrar/proxy port. Send null to omit the port from the SIP URI so\nthe platform resolves the host by DNS SRV (with failover) instead of\nan A record — required for SRV-only hosts. Omitted defaults to 5060.\n"},"transport":{"type":"string","enum":["udp","tcp","tls"],"default":"udp"},"subscribe":{"type":"boolean","default":false},"registrationEnabled":{"type":"boolean","default":true,"description":"Whether the SIP registration is active. Defaults to true when\nomitted. Set to false to disable the registration (mark the agent\ninactive) without deleting its SIP configuration.\n"},"rtpengineEnabled":{"type":"boolean","description":"Whether the rtpengine media relay is engaged for this agent.\nWhen omitted, the agent's stored value is preserved; agents\nwithout a stored value default to true (relay on).\n"},"authUsername":{"type":"string","description":"Optional auth username if different from username"},"did":{"type":"string","description":"Optional DID to send in SIP INVITE"},"sipRoutingHeaderXDestUser":{"type":"string","description":"Optional value for the SIP X-Dest-User routing header.\nAllowed characters: a-z A-Z 0-9 . _ + @ ~ / -\n"},"sipUserAgent":{"type":"string","maxLength":255,"description":"Optional User-Agent header sent on the outbound SIP REGISTER in place\nof the platform default. Use it when the registrar only accepts\nregistrations from specific phone models (for example\n\"Yealink SIP-T54W 96.86.0.75\"). Printable ASCII only, single line,\nat most 255 characters. Omit or send an empty string to use the default.\n"}}},"example":{"sipDomain":"sip.provider.com","username":"1001","password":"secret","proxy":"proxy.provider.com","port":5060,"transport":"udp","subscribe":false,"sipRoutingHeaderXDestUser":"routing.user+1@example","sipUserAgent":"Yealink SIP-T54W 96.86.0.75"}}}},"responses":{"200":{"description":"SIP registration data saved"},"400":{"description":"Bad request (validation failed or invalid DID)"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden - agent is not a LiveKit agent"},"404":{"description":"Agent not found or access denied"},"409":{"description":"Conflict - the DID (did_conflict) or the username@sipDomain pair (sip_aor_conflict) is already in use by another agent"},"500":{"description":"Internal server error"}}},"patch":{"tags":["Agents"],"summary":"Enable or disable SIP Registration for an agent","description":"Toggles the `registrationEnabled` flag on an agent's existing SIP\nregistration without resending the full configuration. Use this to mark\nan agent active/inactive while keeping its stored SIP credentials. The\nagent must already have a SIP registration configured (use PUT first).\n","parameters":[{"in":"path","name":"agentId","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["registrationEnabled"],"properties":{"registrationEnabled":{"type":"boolean","description":"Set to true to enable (register) the agent or false to\ndisable it. All other SIP settings are preserved.\n"}}},"example":{"registrationEnabled":false}}}},"responses":{"200":{"description":"SIP registration enabled state updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden - agent is not a LiveKit agent"},"404":{"description":"Agent not found, access denied, or no SIP registration configured"},"500":{"description":"Internal server error"}}},"delete":{"tags":["Agents"],"summary":"Delete SIP Registration for an agent","description":"Removes SIP registration data for a LiveKit agent.","parameters":[{"in":"path","name":"agentId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"SIP registration data deleted"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden - agent is not a LiveKit agent"},"404":{"description":"Agent not found or access denied"},"500":{"description":"Internal server error"}}}},"/v1/agents/{agentId}/sip/registration/status":{"get":{"tags":["Agents"],"summary":"Get SIP Registration status for an agent","description":"Returns the current OpenSIPS registration state for a LiveKit agent.","parameters":[{"in":"path","name":"agentId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"SIP registration status"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden - agent is not a LiveKit agent"},"404":{"description":"Agent not found, access denied, or no SIP registration configured"},"500":{"description":"Internal server error"}}}},"/v1/agents/{agentId}/status":{"patch":{"tags":["Agents"],"summary":"Update an agent's activation status","description":"Activates or deactivates an agent without sending the rest of its configuration.\n\nAccepts only the two reversible states:\n- `active` — agent is available to receive calls\n- `inactive` — agent is temporarily disabled\n\nIf the agent has SIP registration configured, its `registrationEnabled` flag is\ncarried along in the same write: `inactive` deregisters the agent so it stops\nreceiving calls, `active` registers it again. Agents without SIP registration are\nunaffected beyond the status itself.\n\n`cancelled` (permanently disabled) and `pending` (awaiting activation) are rejected\nwith 400; set those via `PUT /v1/agents/{agentId}` instead. An agent already in\n`cancelled` cannot be revived here and returns 409.\n\nIdempotent: re-sending a status the agent already has, with its registration\nalready in the matching state, succeeds without writing.\n\nNote: `active` agents are the billable ones, so toggling status changes the\naccount's billable agent count on the next quantity sync.\n","parameters":[{"in":"path","name":"agentId","required":true,"schema":{"type":"string","format":"uuid"},"description":"The unique identifier of the agent"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["active","inactive"],"description":"Agent activation status."}}},"example":{"status":"inactive"}}}},"responses":{"200":{"description":"Updated agent. Secret fields (integration passwords/tokens, SIP password) are\nnever included in the response.\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Agent"}}}}}},"400":{"description":"Invalid request data (malformed JSON, missing status, or an unsupported status value)"},"401":{"description":"Unauthorized - missing or invalid token"},"403":{"description":"Forbidden - agent is not a livekit agent"},"404":{"description":"Agent not found or access denied"},"409":{"description":"Agent is cancelled and cannot be reactivated through this endpoint"},"500":{"description":"Internal server error"}}}},"/v1/agents":{"get":{"tags":["Agents"],"summary":"List all agents","description":"Returns a paginated list of agents accessible by the authenticated account. Optionally filter by organization_id or provider.","parameters":[{"in":"query","name":"organization_id","schema":{"type":"string","format":"uuid"},"description":"Filter by organization ID"},{"in":"query","name":"provider","schema":{"type":"string"},"description":"Filter by provider"},{"in":"query","name":"page","schema":{"type":"integer","default":1},"description":"Page number for pagination"},{"in":"query","name":"limit","schema":{"type":"integer","default":50,"maximum":100},"description":"Number of items per page (max 100)"}],"responses":{"200":{"description":"List of agents with pagination info. Secret fields (integration passwords/tokens, SIP password) are never included.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Agent"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"401":{"description":"Unauthorized - missing or invalid token"},"500":{"description":"Internal server error"}}},"post":{"tags":["Agents"],"summary":"Create a new agent","description":"Creates a new agent associated with the specified organization.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAgentRequest"},"example":{"name":"Customer Support Agent","description":"Handles customer inquiries","greetingMessage":"Hello! How can I help you today?","outboundGreetingMessage":"Hi, this is Ava calling from Acme Corp.","systemPrompt":"You are a helpful customer support agent...","agentType":"simple","model":"gpt-4.1","voiceModel":"gpt-4o-realtime-preview","sttModel":"deepgram","ttsModel":"cartesia","voiceSettings":{"voice_id":"alloy"},"transferEnabled":false,"organizationId":"123e4567-e89b-12d3-a456-426614174000"}}}},"responses":{"201":{"description":"Agent created successfully. Secret fields (integration passwords/tokens, SIP password) are never included in the response.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Agent"},"warnings":{"type":"array","items":{"type":"string"},"description":"Present only when configured MCP servers failed their connection autotest; the agent is still created."}}}}}},"400":{"description":"Invalid request data","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"object"}}}}}}},"401":{"description":"Unauthorized - missing or invalid token"},"403":{"description":"Forbidden - no access to the specified organization, or the request enables a feature locked for it in Feature Administration: `warmTransferEnabled: true` (Warm Transfer), a cloned `voiceSettings.voice_id` (Custom Voices), a non-empty `googlePlaceId` (Business Listing), or a gated function in `extensions.tools` (see `extensions`). Clearing a value is never blocked. A `listings_search` tool on an organization where Listings Search is locked gets this 403 even when the tool is also invalid.\n"},"500":{"description":"Internal server error"}}}},"/v1/billing-report":{"get":{"tags":["Billing"],"summary":"Get billing report with usage data","description":"Returns billing data with usage statistics.\nResults are cached in Redis - subsequent requests for same date range return cached data.\n\n**Note:** Input dates use ISO format (YYYY-MM-DD), response dates are in US format (MM/DD/YYYY).\n","parameters":[{"in":"query","name":"start_date","required":true,"schema":{"type":"string","format":"date","example":"2025-12-07"},"description":"Start date (YYYY-MM-DD format)"},{"in":"query","name":"end_date","required":true,"schema":{"type":"string","format":"date","example":"2026-01-06"},"description":"End date (YYYY-MM-DD format)"},{"in":"query","name":"reseller_ids","schema":{"type":"array","items":{"type":"string"}},"description":"Optional - Filter by reseller IDs (comma-separated or array)"},{"in":"query","name":"organization_ids","schema":{"type":"array","items":{"type":"string"}},"description":"Optional - Filter by organization IDs (comma-separated or array)"},{"in":"query","name":"demo_only","schema":{"type":"boolean","default":false},"description":"Optional - Only demo agents (TLZ-683). Only organizations with at least one demo agent are returned; agent_count and voice_minutes cover only those demo agents. Uses the current demo list; never served from cache."}],"responses":{"200":{"description":"Billing report data with dates in US format (MM/DD/YYYY)"},"400":{"description":"Invalid request parameters"},"401":{"description":"Unauthorized"},"403":{"description":"`insufficient_scope` — the API key is limited to a single reseller or organization. This report covers the whole account, so it is open to account-wide keys only; use `/v1/reports/minutes-by-organization` or `/v1/reports/minutes-by-agent`, which a scoped key may call."}}}},"/v1/call-logs/{callLogId}/recording":{"get":{"tags":["Call Logs"],"summary":"Download call recording","description":"Downloads the recording for a specific call log as an audio file.","parameters":[{"in":"path","name":"callLogId","required":true,"schema":{"type":"string","format":"uuid"},"description":"The call log ID"}],"responses":{"200":{"description":"Audio file stream","content":{"audio/mpeg":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Unauthorized - missing or invalid token"},"403":{"description":"Forbidden - no access to this call log"},"404":{"description":"Call log or recording not found"},"500":{"description":"Internal server error"}}}},"/v1/call-logs/{callLogId}/share":{"post":{"tags":["Call Logs"],"summary":"Create a public share link for a call transcript","description":"Creates a public, unauthenticated link to the transcript of a call log — the same\nlink the dashboard's \"Share Transcript\" button produces. The transcript is\nsnapshotted at share time; the link never expires and cannot be revoked. Each call\ncreates a new link.\n\nSharing is refused when the call log's organization has Secure Access enabled or\nhas Transcript Sharing disabled (either via the organization toggle or a\nreseller/partner feature lock).\n","parameters":[{"in":"path","name":"callLogId","required":true,"schema":{"type":"string","format":"uuid"},"description":"The call log ID"}],"responses":{"201":{"description":"Share link created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TranscriptShare"}}}}}},"400":{"description":"callLogId is not a UUID"},"401":{"description":"Unauthorized - missing or invalid token"},"403":{"description":"Forbidden - Secure Access is enabled or Transcript Sharing is disabled for the organization (see `error_description`)"},"404":{"description":"Call log not found (or not accessible to this account)"},"500":{"description":"Internal server error"},"503":{"description":"The organization's sharing policy could not be verified (including an agent with no organization); no link is created — retry"}}}},"/v1/custom-voices/{voiceId}":{"delete":{"tags":["Custom Voices"],"summary":"Delete a custom voice","description":"Deletes an organization's cloned voice. Fails with 409 if any agents are still using the voice; the response lists them.","parameters":[{"in":"path","name":"voiceId","required":true,"schema":{"type":"string"},"description":"Custom voice ID (the VoiceOption id)"},{"in":"query","name":"organizationId","required":true,"schema":{"type":"string","format":"uuid"},"description":"Organization the voice belongs to"}],"responses":{"200":{"description":"Voice deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"organizationId is required"},"401":{"description":"Unauthorized - missing or invalid token"},"404":{"description":"Organization or custom voice not found"},"409":{"description":"Voice is still in use by agents","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"agents":{"type":"array","description":"Agents still using the voice","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}}}}}}}}},"500":{"description":"Internal server error"}}}},"/v1/custom-voices":{"get":{"tags":["Custom Voices"],"summary":"List an organization's custom voices","description":"Returns the organization's cloned voices as catalogue options.","parameters":[{"in":"query","name":"organizationId","required":true,"schema":{"type":"string","format":"uuid"},"description":"Organization whose custom voices to list"}],"responses":{"200":{"description":"Custom voices","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/VoiceOption"}}}}}}},"400":{"description":"organizationId is required"},"401":{"description":"Unauthorized - missing or invalid token"},"404":{"description":"Organization not found or access denied"},"500":{"description":"Internal server error"}}},"post":{"tags":["Custom Voices"],"summary":"Clone a custom voice from an audio sample","description":"Creates a cloned voice for the organization from an uploaded audio clip (max 4MB). Requires the organization's custom-voices feature to be enabled and explicit consent.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["clip","organizationId","name","consent"],"properties":{"clip":{"type":"string","format":"binary","description":"Audio sample of the voice to clone (max 4MB)"},"organizationId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Display name for the cloned voice (must be unique within the organization)"},"language":{"type":"string","default":"en","description":"ISO language code the clone should speak"},"description":{"type":"string"},"consent":{"type":"string","enum":["true"],"description":"Must be \"true\" - confirms consent to clone the voice"}}}}}},"responses":{"201":{"description":"Custom voice created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/VoiceOption"}}}}}},"400":{"description":"Missing/invalid field, unsupported audio type or language, or file too large"},"401":{"description":"Unauthorized - missing or invalid token"},"403":{"description":"Custom voices feature is not enabled for this organization"},"404":{"description":"Organization not found or access denied"},"409":{"description":"A voice with this name already exists"},"500":{"description":"Internal server error"},"502":{"description":"Upstream voice-cloning provider failed"}}}},"/v1/email-servers":{"get":{"tags":["Email Servers"],"summary":"List the account's email servers","description":"Returns the email servers configured for the authenticated account. Use the returned id as the emailServerId field when creating or updating agents. Sensitive fields (SMTP password, SendGrid API key) are never returned.\n\nAn API key limited to a single reseller or organization may call this — an agent names an email server by id, so the list is needed — but receives only `id`, `name`, `type`, `is_default`, `created_at` and `updated_at`. The host, port and from-address fields below are omitted, since they belong to the partner rather than to the reseller or organization the key is scoped to.\n","responses":{"200":{"description":"Email servers, default first. A scoped key receives the reduced column set described above.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"type":{"type":"string","description":"Server type (smtp or sendgrid)"},"is_default":{"type":"boolean"},"smtp_host":{"type":"string","nullable":true},"smtp_port":{"type":"integer","nullable":true},"smtp_from_email":{"type":"string","nullable":true},"smtp_from_name":{"type":"string","nullable":true},"sendgrid_from_email":{"type":"string","nullable":true},"sendgrid_from_name":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}}}}},"401":{"description":"Unauthorized - missing or invalid token"},"500":{"description":"Internal server error"}}}},"/v1/email-templates/{templateId}":{"get":{"tags":["Email Templates"],"summary":"Get an email template","parameters":[{"in":"path","name":"templateId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The template"},"401":{"description":"Unauthorized"},"404":{"description":"Not found / not accessible"}}},"patch":{"tags":["Email Templates"],"summary":"Update an email template","description":"Partial update. organization_id and version are immutable and ignored.","parameters":[{"in":"path","name":"templateId","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":100},"description":{"type":"string","maxLength":300},"subject":{"type":"string","maxLength":998},"body_html":{"type":"string"},"from_name":{"type":"string","maxLength":128},"tags":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["draft","active","archived"]}}}}}},"responses":{"200":{"description":"Updated template"},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Not found / not accessible"},"409":{"description":"Duplicate name"}}},"delete":{"tags":["Email Templates"],"summary":"Delete an email template","description":"Only archived templates may be deleted, and only when no agent references them.","parameters":[{"in":"path","name":"templateId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted"},"401":{"description":"Unauthorized"},"404":{"description":"Not found / not accessible"},"409":{"description":"Not archived","or still referenced by agents":null}}}},"/v1/email-templates":{"get":{"tags":["Email Templates"],"summary":"List email templates","description":"Returns a paginated list of email templates across the authenticated account's organizations. No secrets are ever returned (templates hold no credentials).","parameters":[{"in":"query","name":"organization_id","schema":{"type":"string","format":"uuid"},"description":"Filter to a single organization (must belong to the account)."},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","active","archived"]}},{"in":"query","name":"page","schema":{"type":"integer","default":1,"minimum":1}},{"in":"query","name":"limit","schema":{"type":"integer","default":50,"maximum":100}}],"responses":{"200":{"description":"List of templates with pagination info"},"401":{"description":"Unauthorized"},"403":{"description":"organization_id not owned by the account"}}},"post":{"tags":["Email Templates"],"summary":"Create an email template","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["organization_id","name"],"properties":{"organization_id":{"type":"string","format":"uuid"},"name":{"type":"string","maxLength":100},"description":{"type":"string","maxLength":300},"subject":{"type":"string","maxLength":998},"body_html":{"type":"string"},"from_name":{"type":"string","maxLength":128},"tags":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["draft","active"]}}}}}},"responses":{"201":{"description":"Created template"},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"403":{"description":"organization_id not owned by the account"},"409":{"description":"Duplicate template name in the organization"}}}},"/v1/features":{"get":{"tags":["Features"],"summary":"List feature availability for an account, reseller or organization","description":"Returns every feature in the platform registry with whether it is available or\nlocked for the entity you ask about, so an integration can hide a capability\nbefore a user reaches the write that would be rejected with `403 forbidden`.\n\nChoose the scope with at most one parameter — passing both is rejected:\n\n- **No parameter** — the authenticated account. This reports only locks placed on\n  the account itself, which are the ones that reach every reseller and\n  organization beneath it.\n- **`reseller_id`** — the reseller, including the locks it inherits from the\n  account.\n- **`organization_id`** — the exact view for that organization, including\n  everything it inherits from its reseller and account. This is the scope to use\n  when deciding what to show a specific customer.\n\n`state` is `locked` when the feature is withheld at this scope or above it, and\n`locked_scope` says which: `here` for a lock on the entity you asked about, and\n`inherited` for one placed further up. `decided_by` names what established the\nanswer — `feature_administration` for the feature matrix, `legacy_column` for the\nper-account and per-organization settings that govern while Feature Administration\nis switched off, and `default_available` for a feature nothing withholds.\n\n`feature_administration_enabled` reports which of those two regimes is in force\nplatform-wide. It is included because the same feature can be governed differently\nunder each, and a caller comparing responses over time needs to know which it got.\n\nTwo caveats worth building against:\n\n- **LLM models are the `llm.*` keys, and only those.** An `llm.*` feature reported\n  as available means the model can be chosen; there is no separate per-organization\n  grant any more. `gpt-4.1`, the default model, has no `llm.*` row because it can\n  never be locked. Prefer `GET /v1/llm-models` for building a model picker — it\n  returns the models themselves rather than feature keys.\n- **Opt-in features report permission, not state.** For `secure_access`,\n  `phi_redaction` and `use_reseller_smtp`, `available` means the organization is\n  permitted to switch the feature on, not that it currently is.\n","parameters":[{"in":"query","name":"organization_id","schema":{"type":"string","format":"uuid"},"description":"Optional. Report availability for this organization, applying its account, reseller and organization-level locks. The organization must belong to the authenticated account or the request is rejected with 404. Mutually exclusive with reseller_id.\n"},{"in":"query","name":"reseller_id","schema":{"type":"string","format":"uuid"},"description":"Optional. Report availability for this reseller, applying its own and its account's locks. The reseller must belong to the authenticated account or the request is rejected with 404. Mutually exclusive with organization_id.\n"}],"responses":{"200":{"description":"Feature availability for the requested scope","content":{"application/json":{"schema":{"type":"object","properties":{"feature_administration_enabled":{"type":"boolean","description":"Whether the feature matrix governs, rather than the legacy per-entity settings."},"scope":{"type":"object","properties":{"level":{"type":"string","enum":["account","reseller","organization"]},"id":{"type":"string","format":"uuid"}}},"data":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"category":{"type":"string","enum":["CAPABILITY","TEMPLATE","LLM_MODEL","TTS","INTEGRATION","OPERATIONAL","COMMERCIAL"]},"state":{"type":"string","enum":["available","locked"]},"locked_scope":{"type":"string","nullable":true,"enum":["here","inherited"]},"decided_by":{"type":"string","enum":["feature_administration","legacy_column","default_available"]}}}}}}}}},"400":{"description":"Both organization_id and reseller_id were supplied, or a scope parameter was empty"},"401":{"description":"Unauthorized - missing or invalid token"},"404":{"description":"Organization or reseller not found, or access denied"},"500":{"description":"Internal server error"},"503":{"description":"Availability could not be resolved (transient). Retry; availability is never returned partially resolved."}}}},"/v1/guardrails/{guardrailId}":{"patch":{"tags":["Guardrails"],"summary":"Update a guardrail","description":"Updates one or more fields of a guardrail. At least one field is required.","parameters":[{"in":"path","name":"guardrailId","required":true,"schema":{"type":"string","format":"uuid"},"description":"Guardrail ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"description":{"type":"string","maxLength":1000,"nullable":true},"enabled":{"type":"boolean"}}},"example":{"enabled":false}}}},"responses":{"200":{"description":"Guardrail updated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Invalid request body (at least one field is required)"},"401":{"description":"Unauthorized - missing or invalid token"},"404":{"description":"Guardrail not found or access denied"},"409":{"description":"A guardrail with this name already exists"},"500":{"description":"Internal server error"}}},"delete":{"tags":["Guardrails"],"summary":"Delete a guardrail","parameters":[{"in":"path","name":"guardrailId","required":true,"schema":{"type":"string","format":"uuid"},"description":"Guardrail ID"}],"responses":{"200":{"description":"Guardrail deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"description":"Unauthorized - missing or invalid token"},"404":{"description":"Guardrail not found or access denied"},"500":{"description":"Internal server error"}}}},"/v1/knowledge-bases/{knowledgeBaseId}/reingest":{"post":{"tags":["Knowledge Bases"],"summary":"Re-index every source in a knowledge base","description":"Queues a force-reindex for every source in the knowledge base in one call,\nreplacing each source's existing vectors. Takes no request body.\n\nIndexing is on demand only — there is no scheduled re-crawl — so content that\nchanged after it was first indexed stays stale until a refresh is triggered.\n\nWhat a refresh does per source type:\n* **URL sources** are re-fetched from the live page, so edits made since the\n  last run are picked up.\n* **File sources** are re-processed from their stored copy, which does not\n  change. To pick up an edited document, use\n  `POST /v1/knowledge-sources/{sourceId}/replace` instead.\n\nSources that already have a queued or in-progress job are left alone and\nreported in `skipped`, so calling this twice in a row is safe.\n\nPoll each returned `jobId` via `GET /v1/rag/jobs/{jobId}` to follow progress,\nor poll `GET /v1/knowledge-bases/{knowledgeBaseId}/sources` and wait for every\nsource to leave `pending`.\n","parameters":[{"in":"path","name":"knowledgeBaseId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Re-indexing queued. An empty knowledge base returns sourceCount 0 and no jobs.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"sourceCount":{"type":"integer","description":"Sources found in the knowledge base, including any skipped."},"jobs":{"type":"array","description":"One entry per source actually queued.","items":{"type":"object","properties":{"sourceId":{"type":"string","format":"uuid"},"jobId":{"type":"string","format":"uuid"}}}},"skipped":{"type":"array","description":"Sources left alone because ingestion was already running.","items":{"type":"object","properties":{"sourceId":{"type":"string","format":"uuid"},"reason":{"type":"string","example":"already_in_progress"}}}}}}}}}}},"400":{"description":"The knowledge base has too many sources to refresh in one call"},"401":{"description":"Unauthorized"},"403":{"description":"Knowledge Base is not enabled for this organization"},"404":{"description":"Knowledge base not found or access denied"}}}},"/v1/knowledge-bases/{knowledgeBaseId}":{"put":{"tags":["Knowledge Bases"],"summary":"Update a knowledge base","description":"Renames a knowledge base and/or replaces its description. `name` is required on\nevery call — this is a full replace, not a partial patch. Names must be unique\nwithin the organization.\n","parameters":[{"in":"path","name":"knowledgeBaseId","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string","maxLength":2000,"nullable":true}}},"example":{"name":"Product FAQ (2026)","description":"Updated pricing and support answers"}}}},"responses":{"200":{"description":"Knowledge base updated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Invalid request data"},"401":{"description":"Unauthorized"},"403":{"description":"Knowledge Base is not enabled for this organization"},"404":{"description":"Knowledge base not found or access denied"},"409":{"description":"A knowledge base with that name already exists"}}},"delete":{"tags":["Knowledge Bases"],"summary":"Delete a knowledge base","description":"Deletes the knowledge base and all of its sources, including the stored files and\ntheir indexed vectors. Any agent referencing it has its assignment cleared.\nThis cannot be undone.\n","parameters":[{"in":"path","name":"knowledgeBaseId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Knowledge base deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Knowledge Base is not enabled for this organization"},"404":{"description":"Knowledge base not found or access denied"}}}},"/v1/knowledge-bases/{knowledgeBaseId}/sources":{"get":{"tags":["Knowledge Bases"],"summary":"List sources in a knowledge base","description":"Returns the knowledge base's sources (files and URLs), newest first. Deleted\nsources are omitted. File sources include their stored metadata (`file_name`,\n`file_size`, `file_mime`); the file contents themselves are not served by this API.\n","parameters":[{"in":"path","name":"knowledgeBaseId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"List of sources","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"knowledge_base_id":{"type":"string","format":"uuid"},"organization_id":{"type":"string"},"name":{"type":"string"},"source_type":{"type":"string","enum":["file","url"]},"status":{"type":"string","enum":["pending","active","stale","error"]},"url":{"type":"string","nullable":true},"file_name":{"type":"string","nullable":true},"file_size":{"type":"integer","nullable":true},"file_mime":{"type":"string","nullable":true},"chunk_count":{"type":"integer","nullable":true},"last_synced_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"}}}}}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Knowledge Base is not enabled for this organization"},"404":{"description":"Knowledge base not found or access denied"}}},"post":{"tags":["Knowledge Bases"],"summary":"Add a URL or file source to a knowledge base","description":"Creates a source. The body is discriminated on `sourceType`.\n\n**URL sources** are queued for ingestion immediately; the response returns\n`sourceId` and `jobId`.\n\n**File sources** use a three-step upload: this call returns a `presignedUrl`,\nyou `PUT` the file bytes to it, then call\n`POST /v1/knowledge-sources/{sourceId}/enqueue` with the returned `jobId` to start\ningestion. Supported types are PDF, DOCX, TXT, Markdown, HTML and CSV, up to 50 MB.\n\nDuplicates return `409 duplicate_source` with the existing source in `details`.\nSet `forceReingest: true` to re-ingest the existing source instead.\n","parameters":[{"in":"path","name":"knowledgeBaseId","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["sourceType","url"],"properties":{"sourceType":{"type":"string","enum":["url"]},"url":{"type":"string","format":"uri"},"forceReingest":{"type":"boolean","default":false}}},{"type":"object","required":["sourceType","fileName","fileMime","fileSize"],"properties":{"sourceType":{"type":"string","enum":["file"]},"fileName":{"type":"string","minLength":1,"maxLength":512},"fileMime":{"type":"string","enum":["application/pdf","application/vnd.openxmlformats-officedocument.wordprocessingml.document","text/plain","text/markdown","text/html","text/csv"]},"fileSize":{"type":"integer","minimum":1,"maximum":52428800},"contentHash":{"type":"string","maxLength":128},"forceReingest":{"type":"boolean","default":false}}}]},"example":{"sourceType":"file","fileName":"guide.pdf","fileMime":"application/pdf","fileSize":1048576}}}},"responses":{"201":{"description":"Source created. `presignedUrl` and `fileKey` are returned for file sources only.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"sourceId":{"type":"string","format":"uuid"},"jobId":{"type":"string","format":"uuid"},"presignedUrl":{"type":"string"},"fileKey":{"type":"string"}}}}}}}},"400":{"description":"Invalid request data","or an unsupported fileMime":null},"401":{"description":"Unauthorized"},"403":{"description":"Knowledge Base is not enabled for this organization"},"404":{"description":"Knowledge base not found or access denied"},"409":{"description":"This URL or file already exists in the knowledge base"}}}},"/v1/knowledge-sources/{sourceId}/download":{"get":{"tags":["Knowledge Bases"],"summary":"Download a knowledge source file","description":"Returns a short-lived presigned URL for the source's original uploaded file.\n`GET` that URL to retrieve the bytes — it carries the original filename, so a\nbrowser saves it under the name it was uploaded with rather than the internal\nstorage key.\n\nThe URL expires after `expiresIn` seconds; request a new one rather than\nstoring it. Only **file** sources can be downloaded — a URL source returns\n`400 invalid_request`, and its address is already available in the `url` field\nfrom List Sources.\n","parameters":[{"in":"path","name":"sourceId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Presigned download URL","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"url":{"type":"string","description":"Presigned URL for the file bytes"},"fileName":{"type":"string","nullable":true},"fileMime":{"type":"string","nullable":true},"expiresIn":{"type":"integer","description":"Lifetime of the URL in seconds"}}}}}}}},"400":{"description":"The source is a URL source","not a file":null},"401":{"description":"Unauthorized"},"403":{"description":"Knowledge Base is not enabled for this organization"},"404":{"description":"Source not found","access denied":null,"or it has no stored file":null}}}},"/v1/knowledge-sources/{sourceId}/enqueue":{"post":{"tags":["Knowledge Bases"],"summary":"Start ingestion for an uploaded file source","description":"Third and final step of the file upload flow. Call this after `PUT`-ing the file\nbytes to the `presignedUrl` returned by Create Source, passing the `jobId` from\nthat same response.\n\nOnly file sources can be enqueued — URL sources are queued automatically on\ncreation and return `400 invalid_request` here. The `jobId` must belong to this\nsource, otherwise the call is rejected with `400 invalid_request`.\n\nPoll `GET /v1/rag/jobs/{jobId}` to follow progress.\n","parameters":[{"in":"path","name":"sourceId","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["jobId"],"properties":{"jobId":{"type":"string","format":"uuid"},"forceReindex":{"type":"boolean","default":false}}},"example":{"jobId":"33333333-3333-3333-3333-333333333333"}}}},"responses":{"200":{"description":"Ingestion queued","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Invalid request data","a non-file source":null,"or a jobId that does not belong to this source":null},"401":{"description":"Unauthorized"},"403":{"description":"Knowledge Base is not enabled for this organization"},"404":{"description":"Source not found or access denied"}}}},"/v1/knowledge-sources/{sourceId}/reingest":{"post":{"tags":["Knowledge Bases"],"summary":"Re-ingest an existing source","description":"Re-indexes a source from scratch and replaces its existing vectors. Use this to\nrefresh a URL whose page has changed, or to retry a source left in `error`\nstatus. Works for both file and URL sources, and takes no request body.\n\nPoll the returned `jobId` via `GET /v1/rag/jobs/{jobId}` to follow progress.\n","parameters":[{"in":"path","name":"sourceId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Re-ingestion queued","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"jobId":{"type":"string","format":"uuid"}}}}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Knowledge Base is not enabled for this organization"},"404":{"description":"Source not found or access denied"}}}},"/v1/knowledge-sources/{sourceId}/replace":{"post":{"tags":["Knowledge Bases"],"summary":"Replace a file source's document with an updated version","description":"Returns a presigned URL for uploading a new version of an existing file\nsource's document, overwriting the stored copy in place.\n\nUse this when a document has been edited. Re-ingesting a file source\nre-reads the same stored copy, so it can never pick up an edited document;\nuploading the document again as a new source leaves the outdated version\nindexed alongside the new one. Replacing keeps exactly one source, and its\nid, so anything referencing it keeps working.\n\nThree steps, mirroring file source creation:\n1. `POST` here to get `presignedUrl` and a `jobId`.\n2. `PUT` the file bytes to `presignedUrl`.\n3. `POST /v1/knowledge-sources/{sourceId}/enqueue` with that `jobId` and\n   `forceReindex: true` to rebuild the vectors.\n\nOnly **file** sources can be replaced. A URL source returns `400` — re-ingest\nit instead, which re-fetches the live page.\n","parameters":[{"in":"path","name":"sourceId","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fileName","fileMime","fileSize"],"properties":{"fileName":{"type":"string","description":"Name of the replacement file. Becomes the source's new display name."},"fileMime":{"type":"string","description":"MIME type of the replacement file. May differ from the original."},"fileSize":{"type":"integer"},"contentHash":{"type":"string","description":"Optional hash of the new contents, stored for later duplicate detection."}}}}}},"responses":{"200":{"description":"Presigned upload issued and a job created for the rebuild.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"sourceId":{"type":"string","format":"uuid"},"jobId":{"type":"string","format":"uuid"},"presignedUrl":{"type":"string"},"fileKey":{"type":"string"}}}}}}}},"400":{"description":"Invalid request data","an unsupported fileMime":null,"or the source is not a file source":null},"401":{"description":"Unauthorized"},"403":{"description":"Knowledge Base is not enabled for this organization"},"404":{"description":"Source not found or access denied"}}}},"/v1/knowledge-sources/{sourceId}":{"delete":{"tags":["Knowledge Bases"],"summary":"Delete a knowledge source","description":"Removes a source from its knowledge base, deletes its stored file, and queues\nremoval of its indexed vectors. The agent stops retrieving from it. This cannot\nbe undone — re-add the file or URL to restore it.\n","parameters":[{"in":"path","name":"sourceId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Source deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Knowledge Base is not enabled for this organization"},"404":{"description":"Source not found or access denied"}}}},"/v1/llm-models":{"get":{"tags":["LLM Models"],"summary":"List available LLM models","description":"Returns the language models agents can use. Use the returned id as the agent's model.\n\nThe list is filtered by Feature Administration:\n\n- Without `organization_id`, the response is the floor plus every model NOT locked\n  at the account level. Org/reseller-level locks are enforced on the write path\n  instead — the token only carries an account id.\n- With `organization_id`, the response is the exact view for that organization: the\n  floor plus every model not locked for it at the account, reseller or org level.\n\nThe floor model is always present in both cases.\n","parameters":[{"in":"query","name":"organization_id","schema":{"type":"string","format":"uuid"},"description":"Optional. Scope the catalogue to exactly what this organization may use, applying its account, reseller and org-level Feature Administration locks. The organization must belong to the authenticated account or the request is rejected with 404.\n"}],"responses":{"200":{"description":"LLM models","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"provider":{"type":"string"}}}}}}}}},"401":{"description":"Unauthorized - missing or invalid token"},"404":{"description":"Organization not found or access denied"},"500":{"description":"Internal server error"}}}},"/v1/mcp/test-connection":{"post":{"tags":["MCP"],"summary":"Test an MCP server connection","description":"Validates an MCP (Model Context Protocol) server configuration by opening the\ntransport, performing the initialize handshake, and listing the available tools.\nStateless — nothing is created or stored. Use this to verify a configuration\nbefore assigning it to an agent. A failed connection returns HTTP 200 with\n`success: false`; only malformed requests return 400.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"MCP server endpoint URL","example":"https://mcp.example.com/sse"},"transport_type":{"type":"string","enum":["sse","http"],"default":"sse","description":"Transport protocol (SSE or Streamable HTTP)"},"auth_headers":{"description":"Authentication headers, as an object or an array of key/value pairs","oneOf":[{"type":"object","additionalProperties":{"type":"string"}},{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}}}]},"initialization_options":{"type":"object","description":"Optional MCP initialization options (stored for parity; not exercised in the handshake)"}}}}}},"responses":{"200":{"description":"Test result (success true or false)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"message":{"type":"string"},"toolCount":{"type":"integer"},"tools":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}}}}}}}},"400":{"description":"Invalid request data"},"401":{"description":"Unauthorized - missing or invalid token"},"500":{"description":"Internal server error"}}}},"/v1/me":{"get":{"summary":"Get account ID for the current client","description":"Returns the account_id associated with the client_id in the access token. Requires a valid Bearer access token.","responses":{"200":{"description":"Account ID found","content":{"application/json":{"schema":{"type":"object","properties":{"account_id":{"type":"string"},"scope":{"type":"object","description":"What this key may reach. `type` is `account` for a key over the whole account, or `reseller` / `organization` for one limited to a single reseller or organization, with the matching id alongside.\n","properties":{"type":{"type":"string","enum":["account","reseller","organization"]},"reseller_id":{"type":"string","format":"uuid"},"organization_id":{"type":"string","format":"uuid"}}}}},"example":{"account_id":"m5b6g5jd-lanh-47yi-yhsg-cx56wjdah8h9","scope":{"type":"account"}}}}},"400":{"description":"Invalid or malformed token","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"error_description":{"type":"string"}}},"example":{"error":"invalid_token","error_description":"Token does not contain a valid client_id"}}}},"401":{"description":"Missing or malformed Authorization header","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"error_description":{"type":"string"}}},"example":{"error":"invalid_request","error_description":"Missing or malformed Authorization header"}}}},"404":{"description":"No account_id found for this client_id","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"error_description":{"type":"string"}}},"example":{"error":"not_found","error_description":"No account_id found for this client_id"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"error_description":{"type":"string"}}},"example":{"error":"server_error","error_description":"An unexpected error occurred."}}}}}}},"/v1/metrics":{"get":{"tags":["Metrics"],"summary":"Get usage and performance metrics","description":"Returns aggregated call metrics for a date range. Scope with organizationId or resellerId; with neither, all agents under the account are included. AI-insight fields degrade to null when the analytics backend is unavailable.","parameters":[{"in":"query","name":"startDate","required":true,"schema":{"type":"string","format":"date-time"},"description":"Start of the reporting window (ISO 8601)"},{"in":"query","name":"endDate","required":true,"schema":{"type":"string","format":"date-time"},"description":"End of the reporting window (ISO 8601)"},{"in":"query","name":"organizationId","schema":{"type":"string","format":"uuid"},"description":"Limit to one organization's agents"},{"in":"query","name":"resellerId","schema":{"type":"string","format":"uuid"},"description":"Limit to one reseller's organizations (ignored when organizationId is set)"}],"responses":{"200":{"description":"Aggregated metrics","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"metrics":{"type":"object","description":"Headline numbers: totalCalls, totalMinutes, resolutionRate, avgDuration (seconds), activeAgents, responseTime (average AI model first-token time in ms, nullable — NOT the caller's wait; see turnLatency), sentimentPercent (nullable), escalationRate (nullable)"},"dailyVolume":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","format":"date"},"totalCalls":{"type":"integer"},"resolvedCalls":{"type":"integer"}}}},"resolutionBreakdown":{"type":"object","description":"Counts by outcome: resolved, transferred, abandoned, other"},"hourlyDistribution":{"type":"array","description":"24 entries, one per UTC hour","items":{"type":"object","properties":{"hour":{"type":"string","example":"09:00"},"calls":{"type":"integer"},"minutes":{"type":"integer"}}}},"agentPerformance":{"type":"array","description":"Per-agent call counts, outcome percentages, avgDuration, sentimentPercent, and status; sorted by totalCalls descending","items":{"type":"object"}},"aiInsights":{"type":"object","description":"intentAccuracy, contextRetention, speechRecognition, entityExtraction, responseLatency (average AI model first-token time in ms — NOT the caller's wait; see turnLatency) (all nullable), trainingSessions"},"turnLatency":{"type":"object","description":"TLZ-584 caller wait: LiveKit's measurement from the moment the caller stopped talking to the moment the agent's audio started, in ms. Read from ClickHouse (kept 90 days; measured from this feature's release onward). Replies LiveKit did not measure (greetings, tool-spoken announcements, replies interrupted before any audio) are not counted. Percentiles use ClickHouse quantileExact. Empty when analytics are unavailable.\n","properties":{"overall":{"type":"object","nullable":true,"description":"Null when nothing in the range was measured","properties":{"p50Ms":{"type":"integer"},"p95Ms":{"type":"integer"},"maxMs":{"type":"integer"},"turns":{"type":"integer"}}},"daily":{"type":"array","description":"One entry per UTC day that has data, ascending","items":{"type":"object","properties":{"date":{"type":"string","format":"date"},"p50Ms":{"type":"integer"},"p95Ms":{"type":"integer"},"maxMs":{"type":"integer"},"turns":{"type":"integer"}}}},"byAgent":{"type":"array","description":"Per agent, slowest p95 first","items":{"type":"object","properties":{"agentId":{"type":"string","format":"uuid"},"agentName":{"type":"string"},"p50Ms":{"type":"integer"},"p95Ms":{"type":"integer"},"maxMs":{"type":"integer"},"turns":{"type":"integer"}}}}}},"abandonRate":{"type":"object","description":"TLZ-477 US-4.4 FCC-style abandon rate over fixed rolling windows (24h/7d/30d). NOT scoped by startDate/endDate — the windows are fixed — and covering every accessible agent, including those with no calls in the requested range. The denominator is LIVE answers only (AMD `answer_type: human`), matching how the FCC defines its 3% limit; a window with no live answers reports **null** (\"not measurable\"), never 0, so an agent with AMD disabled is never mistaken for a compliant one.\n","properties":{"org":{"type":"object","description":"Roll-up over the pooled calls of every agent (not an average of their rates). Uses the 0.03 default, since abandon_rate_alert_threshold is a per-agent setting.","properties":{"rates":{"type":"object","description":"Keys 24h, 7d, 30d; each a number 0-1 or null when not measurable"},"threshold":{"type":"number","example":0.03},"breached":{"type":"boolean","description":"True when any measurable window is strictly above the threshold"}}},"byAgent":{"type":"array","description":"Same shape per agent, with that agent's own contactPolicy.abandon_rate_alert_threshold (default 0.03)","items":{"type":"object","properties":{"agentId":{"type":"string","format":"uuid"},"agentName":{"type":"string"},"rates":{"type":"object"},"threshold":{"type":"number"},"breached":{"type":"boolean"}}}}}},"escalationReasons":{"type":"array","items":{"type":"object","properties":{"reason":{"type":"string"},"count":{"type":"integer"},"percentOfTotal":{"type":"integer"},"avgTimeBeforeEscalation":{"type":"integer","description":"Seconds"}}}}}}}}}}},"400":{"description":"Missing required parameters startDate and endDate"},"401":{"description":"Unauthorized - missing or invalid token"},"403":{"description":"No access to the requested organization or reseller"},"500":{"description":"Internal server error"}}}},"/v1/organizations/{organization_id}/agents":{"post":{"tags":["Agents"],"summary":"Create a new agent","description":"Creates a minimal LiveKit agent (title, description, external_id) under the given organization. For creating an agent with full configuration (voice, prompts, model, etc.) use POST /v1/agents instead.\n","parameters":[{"in":"path","name":"organization_id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title"],"properties":{"title":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string","maxLength":1000},"external_id":{"type":"string","maxLength":255}}},"example":{"title":"Support Bot","description":"Handles support tickets","external_id":"external-agent-123"}}}},"responses":{"201":{"description":"Agent created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"ID of the created agent"}}}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized (missing or invalid M2M credentials)"},"404":{"description":"Organization not found, or not owned by the caller's account"},"500":{"description":"Internal server error"}}}},"/v1/organizations/{organization_id}/guardrails":{"get":{"tags":["Guardrails"],"summary":"List an organization's guardrails","description":"Returns all guardrails in the organization, oldest first.","parameters":[{"in":"path","name":"organization_id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Organization ID"}],"responses":{"200":{"description":"Guardrails","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Guardrail"}}}}}}},"401":{"description":"Unauthorized - missing or invalid token"},"404":{"description":"Organization not found or access denied"},"500":{"description":"Internal server error"}}},"post":{"tags":["Guardrails"],"summary":"Create a guardrail","description":"Creates a guardrail in the organization. Names must be unique within the organization.","parameters":[{"in":"path","name":"organization_id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Organization ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":100},"description":{"type":"string","maxLength":1000,"nullable":true},"enabled":{"type":"boolean"}}},"example":{"name":"No PII","description":"Never reveal or confirm a caller's full account number, SSN, or payment card details.","enabled":true}}}},"responses":{"201":{"description":"Guardrail created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Guardrail"}}}}}},"400":{"description":"Invalid request body"},"401":{"description":"Unauthorized - missing or invalid token"},"404":{"description":"Organization not found or access denied"},"409":{"description":"A guardrail with this name already exists"},"500":{"description":"Internal server error"}}}},"/v1/organizations/{organization_id}/knowledge-bases":{"get":{"tags":["Knowledge Bases"],"summary":"List knowledge bases for an organization","description":"Returns the organization's RAG knowledge bases, oldest first, each with a\n`source_count` of its non-deleted sources.\n\nKnowledge Base must be enabled for the organization, otherwise this returns\n`403 forbidden`. It cannot be enabled through the API — enable it in the\nTelzino Dashboard under the organization's feature settings.\n","parameters":[{"in":"path","name":"organization_id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"List of knowledge bases","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organization_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"source_count":{"type":"integer"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Knowledge Base is not enabled for this organization"},"404":{"description":"Organization not found or access denied"}}},"post":{"tags":["Knowledge Bases"],"summary":"Create a knowledge base","description":"Creates an organization-level knowledge base. Names must be unique within the\norganization. Assign it to an agent with Update Agent (`knowledgeBaseId`).\n","parameters":[{"in":"path","name":"organization_id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string","maxLength":2000,"nullable":true}}},"example":{"name":"Product FAQ","description":"Public pricing and support answers"}}}},"responses":{"201":{"description":"Knowledge base created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organization_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"source_count":{"type":"integer"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}}}},"400":{"description":"Invalid request data"},"401":{"description":"Unauthorized"},"403":{"description":"Knowledge Base is not enabled for this organization"},"404":{"description":"Organization not found or access denied"},"409":{"description":"A knowledge base with that name already exists"}}}},"/v1/organizations/{organization_id}/payments/onboarding":{"post":{"summary":"Start (or resume) a merchant's Stripe Connect onboarding","description":"Ensures the organization has a Stripe connected account (creating one on the platform if needed) and returns a Connect Account Session `client_secret` plus the platform `publishable_key`. Pass both to Stripe's embedded onboarding component (`loadConnectAndInitialize` + `ConnectAccountOnboarding`) so the merchant can submit their own banking and identity details directly to Stripe. The publishable key is public by design (it ships in client-side JS); the platform never receives or stores merchant API keys. Safe to call repeatedly: account creation is idempotent per organization and each call mints a fresh, short-lived session secret.\n","parameters":[{"in":"path","name":"organization_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Onboarding session created"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — either the organization is not in your account, or Card Payments (Stripe) is locked for it in Feature Administration. Reading payment status is unaffected; only starting onboarding is blocked.\n"},"404":{"description":"Organization not found"},"500":{"description":"Internal server error"}}}},"/v1/organizations/{organization_id}/payments/status":{"get":{"summary":"Get a merchant's Stripe payment (charge-readiness) status","description":"Returns whether the organization's connected Stripe account is onboarded and able to accept charges. Read-only. Serves the cached account status; pass `?refresh=true` to re-sync from Stripe (slower).\n","parameters":[{"in":"path","name":"organization_id","required":true,"schema":{"type":"string"}},{"in":"query","name":"refresh","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"Payment status"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Organization not found"},"500":{"description":"Internal server error"}}}},"/v1/organizations/{organization_id}":{"delete":{"summary":"Delete an organization","parameters":[{"in":"path","name":"organization_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Organization successfully deleted"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden, or `insufficient_scope` when the API key is limited to a single reseller or organization — deleting an organization is open to account-wide keys only, including the organization the key itself is scoped to."},"404":{"description":"Organization not found"},"409":{"description":"The organization has outbound calls still in flight. Deleting it cascades into its agents and their outbound call records, so the delete is refused until those calls finish. Same status and shape as DELETE /v1/agents/{agentId}.\n"},"500":{"description":"Internal server error"}}}},"/v1/organizations/{organization_id}/suppression-list/{number}":{"delete":{"tags":["Outbound Calls"],"summary":"Remove a number from the outbound suppression list","description":"TLZ-477 US-4.6. The number is E.164, whose leading plus sign is not safe in a URL path: send it percent-encoded (%2B15105550123) or raw. Both forms are accepted and normalised, so a client that encodes differently cannot silently fail to un-suppress a number.\n","parameters":[{"in":"path","name":"organization_id","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"path","name":"number","required":true,"schema":{"type":"string"},"example":"%2B15105550123"}],"responses":{"200":{"description":"Number removed from the suppression list"},"400":{"description":"The number is not valid E.164"},"401":{"description":"Unauthorized - missing or invalid token"},"404":{"description":"Organization not found","or that number is not on the list":null},"500":{"description":"Internal server error"}}}},"/v1/organizations/{organization_id}/suppression-list":{"get":{"tags":["Outbound Calls"],"summary":"List the outbound suppression (do-not-call) entries for an organization","description":"TLZ-477 US-4.6. Paginated deliberately — an unpaginated read is silently capped by the database, and a suppression list that appears to end early is a dangerous thing to show. Entries are scoped per organization, which is NARROWER than the TLZ-476 contact caps: a number suppressed here is still dialable by a sibling organization under the same account.\n","parameters":[{"in":"path","name":"organization_id","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"query","name":"page","schema":{"type":"integer","default":1}},{"in":"query","name":"limit","schema":{"type":"integer","default":50,"maximum":100}}],"responses":{"200":{"description":"One page of suppression entries"},"401":{"description":"Unauthorized - missing or invalid token"},"404":{"description":"Organization not found or access denied"},"500":{"description":"Internal server error"}}},"post":{"tags":["Outbound Calls"],"summary":"Add a number to the outbound suppression list","description":"Idempotent — adding a number already on the list succeeds rather than erroring, since an error here would be a reason not to record a do-not-call request.\n","parameters":[{"in":"path","name":"organization_id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["phone_number"],"properties":{"phone_number":{"type":"string","example":"+15105550123","description":"E.164. Matched EXACTLY against the dialled number - no national-format or fuzzy matching."},"reason":{"type":"string","enum":["opt_out","manual","complaint","litigation","other"],"default":"manual"},"notes":{"type":"string","maxLength":1000},"created_by":{"type":"string","maxLength":200,"description":"Accepted but IGNORED - the author is derived from the authenticated API key, since a caller-supplied value could name anyone in the field this list relies on as evidence."}}}}}},"responses":{"201":{"description":"Number suppressed (or already was)"},"400":{"description":"Invalid request body or phone number"},"401":{"description":"Unauthorized - missing or invalid token"},"404":{"description":"Organization not found or access denied"},"500":{"description":"Internal server error"}}}},"/v1/outbound-calls/export":{"get":{"tags":["Outbound Calls"],"summary":"Export a compliance audit trail of outbound calls","description":"TLZ-477 US-4.5 — the per-campaign audit export. Sourced from outbound calls rather than call transcripts, so EVERY dialled number appears including attempts that were never answered (no_answer, busy, failed); those have no transcript but do carry the consent basis they were placed under, which is what an audit asks about. A date range is required. Requests matching more rows than the cap are refused with 413 rather than truncated — a partial audit file is worse than none, because it cannot be told apart from a complete one.\n","parameters":[{"in":"query","name":"from","required":true,"schema":{"type":"string","format":"date-time"},"description":"Start of the range","inclusive (ISO 8601)":null},{"in":"query","name":"to","required":true,"schema":{"type":"string","format":"date-time"},"description":"End of the range","inclusive (ISO 8601)":null},{"in":"query","name":"campaign_id","schema":{"type":"string"},"description":"Filter to one campaign, matched against the reserved `metadata.campaign_id` key supplied on Trigger Outbound Call"},{"in":"query","name":"agent_id","schema":{"type":"string","format":"uuid"},"description":"Filter to one agent"},{"in":"query","name":"status","schema":{"type":"string"},"description":"Filter to one call status"},{"in":"query","name":"format","schema":{"type":"string","enum":["csv","json"],"default":"csv"},"description":"CSV downloads as an attachment; JSON returns the same columns under `data`"}],"responses":{"200":{"description":"The audit export. A CSV with no matching calls still carries its header row, so \"nothing matched\" is distinguishable from a failed export.","content":{"text/csv":{"schema":{"type":"string"}},"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object"}},"row_count":{"type":"integer"}}}}}},"400":{"description":"Missing or malformed from/to range"},"401":{"description":"Unauthorized - missing or invalid token"},"413":{"description":"Too many rows for one export (error: export_too_large); narrow the range or add filters"},"500":{"description":"Internal server error"}}}},"/v1/outbound-calls/queued":{"delete":{"tags":["Outbound Calls"],"summary":"Cancel all scheduled retries for a phone number","description":"TLZ-476 US-3.3 — cancels every still-scheduled outbound retry to the given number across the caller's account, for on-demand opt-out / DNC handling when only the phone number is known. Retries that have already started dialing are left untouched. Account-scoped: one account can never cancel another's retries.\n","parameters":[{"in":"query","name":"to_number","required":true,"schema":{"type":"string"},"description":"E.164 destination number, e.g. +15105550123"}],"responses":{"200":{"description":"Cancelled; response includes the number of retries cancelled"},"400":{"description":"Missing or invalid to_number"},"401":{"description":"Unauthorized"},"500":{"description":"Failed to cancel retries"}}}},"/v1/payments/merchants":{"get":{"summary":"List the caller's merchants with Stripe charge-readiness","description":"Returns every organization (merchant) owned by the authenticated account together with its Stripe Connect status, in a single call. Read-only and strictly scoped to the caller's own organizations. Serves cached status (no per-merchant Stripe round trip); use `/v1/organizations/{organization_id}/payments/status?refresh=true` to force a re-sync for one merchant.\n","responses":{"200":{"description":"List of merchants"},"401":{"description":"Unauthorized"},"500":{"description":"Internal server error"}}}},"/v1/payments/transactions":{"get":{"summary":"List payment transactions across the caller's merchants","description":"Read-only payment ledger, strictly scoped to organizations owned by the authenticated account. Customer references are masked. Supports filtering by status, organization, and a free-text search over Stripe ids / customer.\n","parameters":[{"in":"query","name":"status","schema":{"type":"string","enum":["created","paid","abandoned","failed","refunded"]}},{"in":"query","name":"organization_id","schema":{"type":"string"}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"limit","schema":{"type":"integer","default":50,"maximum":100}},{"in":"query","name":"offset","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"List of transactions"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"500":{"description":"Internal server error"}}}},"/v1/places/search":{"get":{"tags":["Places"],"summary":"Search Google business listings","description":"Searches Google Places (Autocomplete with Text Search fallback) for a business.\nReturns place suggestions you can link to an agent via the `googlePlaceId` field\non Create/Update Agent. Queries shorter than 3 characters return an empty list.\n","parameters":[{"in":"query","name":"q","required":true,"schema":{"type":"string","maxLength":256},"description":"Business name or address to search for."}],"responses":{"200":{"description":"Matching suggestions","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"placeId":{"type":"string"},"text":{"type":"string"}}}}}}}}},"400":{"description":"Missing or overlong q"},"401":{"description":"Unauthorized"},"403":{"description":"Business Listing (Google Places) is locked for this account in Feature Administration"},"503":{"description":"Places search not configured"}}}},"/v1/profile":{"get":{"tags":["Profile"],"summary":"Get the logged-in dashboard user's profile","description":"Returns the Auth0 user profile from the dashboard session cookie. This endpoint authenticates via the encrypted session cookie, not a bearer token - it is intended for the dashboard UI, not machine-to-machine API clients.","security":[],"responses":{"200":{"description":"Auth0 user profile of the session user","content":{"application/json":{"schema":{"type":"object","properties":{"sub":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"picture":{"type":"string"}}}}}},"401":{"description":"No user session found"},"500":{"description":"Internal server error"}}}},"/v1/rag/jobs/{jobId}":{"get":{"tags":["Knowledge Bases"],"summary":"Get ingestion job status","description":"Polls the progress of an ingestion job returned by Create Source, Enqueue, or\nRe-ingest. A source is only searchable by the agent once its job has finished and\nthe source reaches `active` status.\n\n`chunks_processed` and `chunks_total` are populated while indexing is running;\n`error_text` explains a failed job.\n","parameters":[{"in":"path","name":"jobId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Job status","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"status":{"type":"string"},"stage":{"type":"string","nullable":true},"chunks_processed":{"type":"integer","nullable":true},"chunks_total":{"type":"integer","nullable":true},"error_text":{"type":"string","nullable":true},"source_id":{"type":"string","format":"uuid"}}}}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Knowledge Base is not enabled for this organization"},"404":{"description":"Job not found or access denied"}}}},"/v1/reports/minutes-by-agent":{"get":{"summary":"Get minutes usage by agent","description":"Returns minutes usage aggregated by agent for the requested date range.\nData is sourced from LiveKit voice minutes in ClickHouse.\n","tags":["Reports"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"start_date","required":true,"schema":{"type":"string","format":"date","example":"2026-04-01"},"description":"Start date in YYYY-MM-DD"},{"in":"query","name":"end_date","required":true,"schema":{"type":"string","format":"date","example":"2026-04-13"},"description":"End date in YYYY-MM-DD"},{"in":"query","name":"agent_ids","required":true,"schema":{"type":"array","items":{"type":"string","format":"uuid"}},"style":"form","explode":false,"description":"Agent UUIDs as comma-separated values or repeated `agent_ids[]` params"}],"responses":{"200":{"description":"Minutes-by-agent report","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"agent_id":{"type":"string","format":"uuid"},"agent_name":{"type":"string"},"reseller_name":{"type":"string"},"total_minutes":{"type":"number","format":"float"},"call_count":{"type":"integer"}}}},"period":{"type":"object","properties":{"start_date":{"type":"string","format":"date"},"end_date":{"type":"string","format":"date"}}}}}}}},"400":{"description":"Invalid request parameters"},"401":{"description":"Unauthorized"},"404":{"description":"No matching agents found for this account"},"500":{"description":"Internal server error"}}}},"/v1/reports/minutes-by-organization":{"get":{"summary":"Get minutes usage grouped by organization with agent breakdown","description":"Returns voice minutes aggregated per organization for the requested date range,\nwith a nested per-agent breakdown. Data is sourced from LiveKit voice minutes in ClickHouse.\nIf `organization_ids` / `reseller_ids` are omitted, all organizations the caller has access to are returned.\n","tags":["Reports"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"start_date","required":true,"schema":{"type":"string","format":"date","example":"2026-04-01"}},{"in":"query","name":"end_date","required":true,"schema":{"type":"string","format":"date","example":"2026-04-13"}},{"in":"query","name":"organization_ids","schema":{"type":"array","items":{"type":"string","format":"uuid"}},"style":"form","explode":false,"description":"Optional filter by organization UUIDs (comma-separated or `organization_ids[]`)"},{"in":"query","name":"reseller_ids","schema":{"type":"array","items":{"type":"string","format":"uuid"}},"style":"form","explode":false,"description":"Optional filter by reseller UUIDs (comma-separated or `reseller_ids[]`)"}],"responses":{"200":{"description":"Minutes-by-organization report","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"organization_id":{"type":"string","format":"uuid"},"organization_name":{"type":"string"},"reseller_id":{"type":"string","format":"uuid"},"reseller_name":{"type":"string"},"total_voice_minutes":{"type":"number","format":"float"},"agent_count":{"type":"integer"},"agents":{"type":"array","items":{"type":"object","properties":{"agent_id":{"type":"string","format":"uuid"},"agent_name":{"type":"string"},"voice_minutes":{"type":"number","format":"float"}}}}}}},"period":{"type":"object","properties":{"start_date":{"type":"string","format":"date"},"end_date":{"type":"string","format":"date"}}}}}}}},"400":{"description":"Invalid request parameters"},"401":{"description":"Unauthorized"},"500":{"description":"Internal server error"}}}},"/v1/resellers/{resellerId}/organizations":{"get":{"summary":"Get organizations for a reseller","parameters":[{"in":"path","name":"resellerId","required":true,"schema":{"type":"string"}},{"in":"query","name":"external_id","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"List of organizations"},"400":{"description":"Invalid resellerId (must be a UUID)"},"401":{"description":"Unauthorized. Also `invalid_token` when the token was minted before the API key's secret was last rotated; request a new token."},"403":{"description":"`insufficient_scope` — the API key is limited to a single reseller or organization. Use `GET /v1/agents` or the reports, which a scoped key may call."},"404":{"description":"Reseller not found"},"500":{"description":"Internal server error"},"503":{"description":"`temporarily_unavailable` — the API key's scope could not be verified. Retry shortly."}}},"post":{"summary":"Create a new organization for a reseller","parameters":[{"in":"path","name":"resellerId","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"orgName":{"type":"string"},"external_id":{"type":"string"}}},"example":{"orgName":"Acme Solar","external_id":"external-org-12345"}}}},"responses":{"200":{"description":"An organization with this `external_id` already exists under the\nreseller; the existing record is returned and nothing was created.\nCreation is idempotent on `external_id`.\n"},"201":{"description":"Organization created"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized (missing or invalid M2M credentials)"},"403":{"description":"`insufficient_scope` — the API key is limited to a single reseller or organization and cannot create organizations."},"404":{"description":"Reseller not found, or not owned by the caller's account"},"500":{"description":"Internal server error"}}}},"/v1/resellers/{resellerId}":{"delete":{"summary":"Delete a reseller","parameters":[{"in":"path","name":"resellerId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Reseller successfully deleted"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden, or `insufficient_scope` when the API key is limited to a single reseller or organization — deleting a reseller is open to account-wide keys only."},"404":{"description":"Reseller not found"},"500":{"description":"Internal server error"}}}},"/v1/reservations/analytics":{"get":{"summary":"Reservation agent analytics","description":"Call outcomes, drop-off, SMS delivery and third-party API usage for\nOpenTable Reservation agents, over a date range.\n\nScoped to the authenticated account. Narrow further with `organization_id`\nor `agent_id`. Third-party API **cost** is never returned — StayAPI and\nTwilio Lookup are funded at the Platform tier, so only call counts are\nexposed, for capacity and anomaly detection.\n","tags":["Reports"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"start_date","required":true,"schema":{"type":"string","format":"date","example":"2026-08-01"}},{"in":"query","name":"end_date","required":true,"schema":{"type":"string","format":"date","example":"2026-08-17"}},{"in":"query","name":"organization_id","required":false,"schema":{"type":"string","format":"uuid"}},{"in":"query","name":"agent_id","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Aggregated reservation analytics"},"400":{"description":"Invalid query parameters"},"401":{"description":"Unauthorized"}}}},"/v1/retry-policies/{policyId}":{"get":{"tags":["Retry Policies"],"summary":"Get a retry policy","responses":{"200":{"description":"The retry policy"},"404":{"description":"Not found"}}},"patch":{"tags":["Retry Policies"],"summary":"Update a retry policy","description":"Partial update. Editing a policy does NOT change retry chains already in flight — each call carries its own snapshot of the rules taken when it was created.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"rules":{"type":"object","description":"Replaces the whole rules object. Keys: no_answer, busy, voicemail, failed."},"contact_policy":{"type":"object","nullable":true},"on_exhausted":{"type":"string","enum":["end","webhook","notify_email"]},"on_exhausted_target":{"type":"string","nullable":true}}}}}},"responses":{"200":{"description":"Updated policy"},"400":{"description":"Invalid request"},"403":{"description":"`insufficient_scope` — retry policies belong to the account, so a key limited to a single reseller or organization can read them but not change them"},"404":{"description":"Not found"},"409":{"description":"A policy with that name already exists"}}},"delete":{"tags":["Retry Policies"],"summary":"Delete a retry policy","description":"Agents and calls referencing it fall back to \"no retries\". Chains already in flight are unaffected — they keep their own snapshot of the rules.","responses":{"204":{"description":"Deleted"},"403":{"description":"`insufficient_scope` — retry policies belong to the account, so a key limited to a single reseller or organization can read them but not delete them"},"404":{"description":"Not found"}}}},"/v1/retry-policies":{"get":{"tags":["Retry Policies"],"summary":"List retry policies","description":"Account-level outbound retry rulebooks. Use an `id` as `retry_policy_id` when triggering an outbound call, or as `defaultRetryPolicyId` on an agent.","responses":{"200":{"description":"Retry policy list"}}},"post":{"tags":["Retry Policies"],"summary":"Create a retry policy","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","example":"Standard","description":"Unique within the account"},"rules":{"type":"object","description":"Per-outcome retry rules. Keys: no_answer, busy, voicemail, failed. Each is { delay_seconds, max_attempts, backoff: linear|exponential }. An omitted outcome is never retried; `completed` is not a valid key.","example":{"no_answer":{"delay_seconds":10800,"max_attempts":3,"backoff":"linear"},"busy":{"delay_seconds":1200,"max_attempts":4,"backoff":"exponential"}}},"contact_policy":{"type":"object","description":"Hard caps per destination number, enforced across the account: { max_attempts_per_day, max_attempts_per_week, min_seconds_between_attempts }.","example":{"max_attempts_per_day":3,"max_attempts_per_week":7,"min_seconds_between_attempts":1800}},"on_exhausted":{"type":"string","enum":["end","webhook","notify_email"],"description":"What to do once a chain's attempts are used up"},"on_exhausted_target":{"type":"string","description":"Required unless on_exhausted is \"end\" — an https URL for webhook","an email address for notify_email":null}}}}}},"responses":{"201":{"description":"Policy created"},"400":{"description":"Invalid request"},"403":{"description":"`insufficient_scope` — retry policies belong to the account, so a key limited to a single reseller or organization can read them but not create them"},"409":{"description":"A policy with that name already exists"}}}},"/v1/sessions/netsapiens":{"post":{"summary":"Create session token (NetSapiens API Key)","description":"Used by NetSapiens frontend JavaScript for direct integration","tags":["Sessions"],"parameters":[{"in":"header","name":"X-NS-Api-Key","required":true,"schema":{"type":"string"},"description":"NetSapiens API token from OAuth","example":"ns_api_token_from_netsapiens_oauth"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["profile_id","claim_id"],"properties":{"profile_id":{"type":"string","format":"uuid","description":"Configuration profile ID","example":"123e4567-e89b-12d3-a456-426614174000"},"claim_id":{"type":"string","format":"uuid","description":"NetSapiens user base claim ID","example":"9f14c1a4-02f0-4ad0-b5f4-08c1f8f9b3a2"},"user_id":{"type":"string","description":"Optional external user identifier","example":"optional-external-user-id"}}}}}},"responses":{"200":{"description":"Session token created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","description":"Session token","example":"abc123xyz"},"expires_at":{"type":"string","format":"date-time","description":"Token expiration time","example":"2025-07-02T18:30:00Z"}}}}}},"400":{"description":"Bad request - missing required fields"},"401":{"description":"Unauthorized - invalid NetSapiens API key. Also `invalid_token` when a Telzino bearer token is presented that was minted before that API key's secret was last rotated; request a new token.\n"},"403":{"description":"`insufficient_scope` — only when a Telzino API-key bearer token is presented alongside the NetSapiens key AND that key is limited to a single reseller or organization. Callers authenticating with `X-NS-Api-Key` alone are unaffected.\n"},"404":{"description":"Claim not found"},"500":{"description":"Internal server error"},"503":{"description":"`temporarily_unavailable` — a Telzino bearer token was presented and its API key's scope could not be verified. Retry shortly.\n"}}}},"/v1/sessions":{"post":{"summary":"Create session token (General OAuth2)","description":"Used by 3rd-party backend services using Telzino Account API Keys","tags":["Sessions"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["profile_id"],"properties":{"profile_id":{"type":"string","format":"uuid","description":"Configuration profile ID","example":"123e4567-e89b-12d3-a456-426614174000"},"user_id":{"type":"string","description":"Optional external user identifier","example":"1234-5678"}}}}}},"responses":{"200":{"description":"Session token created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","description":"Session token","example":"abc123xyz"},"expires_at":{"type":"string","format":"date-time","description":"Token expiration time","example":"2025-07-02T18:30:00Z"}}}}}},"400":{"description":"Bad request - missing required fields"},"401":{"description":"Unauthorized - invalid or missing authorization. Also `invalid_token` when the token was minted before the API key's secret was last rotated; request a new token."},"403":{"description":"`insufficient_scope` — the API key is limited to a single reseller or organization. This endpoint mints a session for any user of the account, so it is open to account-wide keys only."},"404":{"description":"Profile not found"},"500":{"description":"Internal server error"},"503":{"description":"`temporarily_unavailable` — the API key's scope could not be verified. Retry shortly."}}}},"/v1/sms-templates/{templateId}":{"get":{"tags":["SMS Templates"],"summary":"Get an SMS template","parameters":[{"in":"path","name":"templateId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The template"},"401":{"description":"Unauthorized"},"404":{"description":"Not found / not accessible"}}},"patch":{"tags":["SMS Templates"],"summary":"Update an SMS template","description":"Partial update. message_type, organization_id, media_key and version are immutable and are REJECTED with 400 — the schema is strict, so an unknown or server-managed key fails the request rather than being silently dropped. This annotation feeds the generated OpenAPI spec; it previously said \"ignored\", which contradicted the code and the published docs.","parameters":[{"in":"path","name":"templateId","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":100},"description":{"type":"string","maxLength":300},"body":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"from_number":{"type":"string"},"status":{"type":"string","enum":["draft","active","archived"]},"is_first_contact":{"type":"boolean"},"allowed_regions":{"type":"array","items":{"type":"string"}},"expected_version":{"type":"integer","description":"Optional optimistic-concurrency guard. Pass the `version` you read; a 409 is returned if the template changed since."}}}}}},"responses":{"200":{"description":"Updated template"},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"403":{"description":"SMS Template Manager is locked for this organization (does not apply to an archive-only patch)"},"404":{"description":"Not found / not accessible"},"409":{"description":"Duplicate name","or version conflict (the template was changed since it was read)":null}}},"delete":{"tags":["SMS Templates"],"summary":"Delete an SMS template","description":"Only archived templates may be deleted, and only when no agent references them.","parameters":[{"in":"path","name":"templateId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted"},"401":{"description":"Unauthorized"},"404":{"description":"Not found / not accessible"},"409":{"description":"Not archived","or still referenced by agents":null}}}},"/v1/sms-templates":{"get":{"tags":["SMS Templates"],"summary":"List SMS templates","description":"Returns a paginated list of SMS templates across the authenticated account's organizations. No secrets are ever returned (templates hold no credentials).","parameters":[{"in":"query","name":"organization_id","schema":{"type":"string","format":"uuid"},"description":"Filter to a single organization (must belong to the account)."},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","active","archived"]}},{"in":"query","name":"message_type","schema":{"type":"string","enum":["sms","mms"]}},{"in":"query","name":"page","schema":{"type":"integer","default":1,"minimum":1}},{"in":"query","name":"limit","schema":{"type":"integer","default":50,"maximum":100}}],"responses":{"200":{"description":"List of templates with pagination info"},"401":{"description":"Unauthorized"},"403":{"description":"organization_id not owned by the account"}}},"post":{"tags":["SMS Templates"],"summary":"Create an SMS template","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["organization_id","name","message_type"],"properties":{"organization_id":{"type":"string","format":"uuid"},"name":{"type":"string","maxLength":100},"message_type":{"type":"string","enum":["sms","mms"]},"description":{"type":"string","maxLength":300},"body":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"from_number":{"type":"string"},"status":{"type":"string","enum":["draft","active"]}}}}}},"responses":{"201":{"description":"Created template"},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"403":{"description":"organization_id not owned by the account","or SMS Template Manager is locked for it":null},"409":{"description":"Duplicate template name in the organization"}}}},"/v1/stt-providers":{"get":{"tags":["STT Providers"],"summary":"List available speech-to-text providers","description":"Returns the STT providers agents can use. Use the returned id as the agent's sttModel.","responses":{"200":{"description":"STT providers","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"}}}}}}}}},"401":{"description":"Unauthorized - missing or invalid token"},"500":{"description":"Internal server error"}}}},"/v1/tools/check-mobile":{"get":{"tags":["Tools"],"summary":"Check whether a phone number is a mobile phone","description":"Looks up a phone number's line type. Unknown, anonymous, or too-short numbers return a graceful valid=false result instead of an error. Used by the agent runtime (e.g. to decide whether SMS is possible).","parameters":[{"in":"query","name":"number","required":true,"schema":{"type":"string"},"description":"Phone number to check"}],"responses":{"200":{"description":"Lookup result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MobileCheckResult"}}}},"400":{"description":"Phone number parameter is required"},"500":{"description":"Lookup failed or internal server error"}}},"post":{"tags":["Tools"],"summary":"Check whether a phone number is a mobile phone (webhook form)","description":"Webhook variant used by the agent runtime. Provide the number as caller_id, number, or phone_number.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"caller_id":{"type":"string"},"number":{"type":"string"},"phone_number":{"type":"string"},"agent_name":{"type":"string","description":"Optional context from the agent runtime; ignored for the lookup"}}},"example":{"caller_id":"+14155552671"}}}},"responses":{"200":{"description":"Lookup result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MobileCheckResult"}}}},"400":{"description":"One of caller_id, number, or phone_number is required"},"500":{"description":"Lookup failed or internal server error"}}}},"/v1/voices/preview":{"post":{"tags":["Voices"],"summary":"Generate a voice preview clip","description":"Synthesizes a short MP3 preview of the given voice. If no text is supplied, a default greeting in the voice's language (English, Spanish, or French) is used.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["voiceId"],"properties":{"voiceId":{"type":"string","description":"Voice ID from GET /v1/voices"},"text":{"type":"string","maxLength":500,"description":"Text to speak (defaults to a greeting in the voice's language)"}}},"example":{"voiceId":"694f9389-aac1-45b6-b726-9d9369183238","text":"Thanks for calling Acme Corp, how can I help?"}}}},"responses":{"200":{"description":"Preview generated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"audioData":{"type":"string","description":"Base64-encoded MP3 audio"}}}}}},"400":{"description":"Invalid request data"},"401":{"description":"Unauthorized - missing or invalid token"},"500":{"description":"Internal server error"},"502":{"description":"Upstream TTS provider failed to generate the preview"},"503":{"description":"Voice preview not configured on this environment"}}}},"/v1/voices":{"get":{"tags":["Voices"],"summary":"List available voices","description":"Returns the full voice catalogue. When an accessible organizationId is supplied the catalogue is tailored to that organization - the voices of any TTS vendor locked for it are omitted (TLZ-505), and if it has the custom-voices feature enabled its custom (cloned) voices are merged in. An organizationId you cannot access is ignored and the full catalogue is returned.","parameters":[{"in":"query","name":"organizationId","schema":{"type":"string","format":"uuid"},"description":"Tailor the catalogue to this organization - omits locked TTS vendors, and merges in its custom voices when that feature is enabled. Requires access to the organization; an inaccessible id is ignored."}],"responses":{"200":{"description":"Voice catalogue","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/VoiceOption"}}}}}}},"401":{"description":"Unauthorized - missing or invalid token"},"500":{"description":"Internal server error"}}}}},"tags":[{"name":"Agents","description":"Agent management endpoints"},{"name":"Billing","description":"Billing report endpoints with caching"},{"name":"Custom Voices","description":"Organization-scoped cloned voices (requires the custom-voices feature)"},{"name":"Email Servers","description":"Email servers configured for the account"},{"name":"Email Templates","description":"Reusable email templates the send_email voice-agent tool renders at call time (TLZ-470)."},{"name":"Features","description":"Feature availability and locks"},{"name":"LLM Models","description":"Language-model catalogue"},{"name":"Metrics","description":"Usage and performance metrics"},{"name":"Guardrails","description":"Organization-level guardrails applied to all agents in the organization"},{"name":"Profile","description":"Dashboard user profile"},{"name":"SMS Templates","description":"Reusable SMS message templates the send_sms voice-agent tool renders at call time (TLZ-471)."},{"name":"STT Providers","description":"Speech-to-text provider catalogue"},{"name":"Tools","description":"Utility endpoints used by the agent runtime"},{"name":"Voices","description":"Voice catalogue and preview endpoints"}]}