API reference

Every endpoint of API v1. Examples in curl, JavaScript (fetch) and Python (requests); the language you pick is remembered across the page.

Base URLhttps://hubvocal.com/v1
AuthAuthorization: Bearer vt_live_…
Rate limit120 requests/min per key · 429 + Retry-After
Scopesread · write · calls
Each key has read, write and/or calls. read covers GET requests, write covers changes, calls covers outbound calls. A request outside the key's scopes gets 403.
Error body{"detail": "..."}
OpenAPIGET /v1/openapi.json

For authentication, embedding and error codes, see Developers.

Agents

The conversations run in text mode, so they test instructions and tool use, not voice or latency. Simulations are run from the dashboard; there is no /v1 endpoint for them yet.

List agents

GET /v1/agents

Returns every agent in your organization, in the order shown in the dashboard.

Request

curl
curl "https://hubvocal.com/v1/agents" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
[
  {
    "id": "front-desk",
    "name": "Front desk",
    "lang": "en",
    "engine": "live",
    "instructions": "You are the front desk assistant for a dental clinic. Help callers book appointments and answer general questions briefly. Never make things up; offer a callback from the team instead.",
    "greeting": "Hello, thanks for calling the clinic. How can I help you today?",
    "voice": {
      "voice": "marin",
      "speed": 1
    },
    "tools": {
      "builtins": [
        "end_call",
        "take_message"
      ],
      "webhooks": [
        "tl_8f3a2c"
      ]
    },
    "turn": {
      "first_speaker": "agent",
      "idle_check_s": 30
    },
    "recording": {
      "enabled": false,
      "announce": true
    },
    "post_call": {
      "summary": true,
      "fields": [
        {
          "name": "appointment_date",
          "type": "date",
          "description": "Requested appointment date, if any"
        },
        {
          "name": "callback",
          "type": "boolean",
          "description": "Does the caller want a callback?"
        }
      ]
    },
    "knowledge": {
      "doc_ids": [
        "kb_7d21e4"
      ],
      "mode": "tool"
    },
    "published_version": 3,
    "draft_changed": false
  }
]

Create an agent

POST /v1/agents

Creates an agent. Every field is validated on the server; values over the limits are trimmed. If you pass an id that already exists, the request fails with 409. Use PUT to update.

NameInTypeDescription
id body string Optional. Lowercase letters, digits and hyphens, up to 48 characters. Generated from the name when omitted.
name
required
body string Display name, up to 60 characters.
lang
required
body string Conversation language code, e.g. tr, en, de. The full list is in GET /v1/voices.
engine
required
body "live" | "gemini" | "realtime" | "classic" live: GPT-Live 1, gemini: Gemini Live, realtime: OpenAI Realtime, classic: STT → LLM → TTS.
instructions body string System instructions, up to 8000 characters.
greeting body string What the agent says first, up to 400 characters.
voice body object voice (a name from /v1/voices), speed (classic and realtime only; fixed at 1.0 for live and gemini), tone (free-text speaking style, up to 600 characters).
tools body object builtins (end_call, take_message) and webhooks (tool ids); up to 16 tools in total. Empty or missing means no tools. mcp: [{server_id, tools}] adds tools from your MCP servers; they count towards the same limit.
turn body object Turn-taking settings: sensitivity (low, balanced, high; classic engine only), first_speaker (agent or caller), silence_wait_s (1–5), idle_check_s (0 or 10–120), agent_timeout_s (0 or 10–60). Empty fields use the defaults.
recording body object recording: {enabled (default false), announce (default true), announce_text?}. When on, the agent announces the recording before the greeting.
post_call body object post_call.summary: write a summary when the call ends (default true). post_call.fields: up to 24 fields to fill after the call ({name, type, description, enum?, required?}).
knowledge body object doc_ids (up to 50 knowledge base documents) and mode ("tool"). The agent then gets the search_knowledge tool.
calendar body object integration_id and calendar_id from the dashboard, duration_min (15–240), buffer_min (0–60), timezone, working_hours, min_notice_h (0–72), max_days_ahead (1–90). Enables the appointment tools.

Request

curl
curl -X POST "https://hubvocal.com/v1/agents" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Front desk",
  "lang": "en",
  "engine": "live",
  "instructions": "You are the front desk assistant for a dental clinic. Help callers book appointments and answer general questions briefly. Never make things up; offer a callback from the team instead.",
  "greeting": "Hello, thanks for calling the clinic. How can I help you today?",
  "voice": {
    "voice": "marin",
    "speed": 1
  },
  "tools": {
    "builtins": [
      "end_call",
      "take_message"
    ],
    "webhooks": [
      "tl_8f3a2c"
    ]
  },
  "turn": {
    "first_speaker": "agent",
    "idle_check_s": 30
  },
  "recording": {
    "enabled": false,
    "announce": true
  },
  "post_call": {
    "summary": true,
    "fields": [
      {
        "name": "appointment_date",
        "type": "date",
        "description": "Requested appointment date, if any"
      },
      {
        "name": "callback",
        "type": "boolean",
        "description": "Does the caller want a callback?"
      }
    ]
  },
  "knowledge": {
    "doc_ids": [
      "kb_7d21e4"
    ],
    "mode": "tool"
  }
}'

Response · 201

JSON
{
  "id": "front-desk",
  "name": "Front desk",
  "lang": "en",
  "engine": "live",
  "instructions": "You are the front desk assistant for a dental clinic. Help callers book appointments and answer general questions briefly. Never make things up; offer a callback from the team instead.",
  "greeting": "Hello, thanks for calling the clinic. How can I help you today?",
  "voice": {
    "voice": "marin",
    "speed": 1
  },
  "tools": {
    "builtins": [
      "end_call",
      "take_message"
    ],
    "webhooks": [
      "tl_8f3a2c"
    ]
  },
  "turn": {
    "first_speaker": "agent",
    "idle_check_s": 30
  },
  "recording": {
    "enabled": false,
    "announce": true
  },
  "post_call": {
    "summary": true,
    "fields": [
      {
        "name": "appointment_date",
        "type": "date",
        "description": "Requested appointment date, if any"
      },
      {
        "name": "callback",
        "type": "boolean",
        "description": "Does the caller want a callback?"
      }
    ]
  },
  "knowledge": {
    "doc_ids": [
      "kb_7d21e4"
    ],
    "mode": "tool"
  },
  "published_version": null,
  "draft_changed": true
}

Errors

  • 409 An agent with this id already exists.

Retrieve an agent

GET /v1/agents/{id}

Returns a single agent. published_version is the live version number (null if it was never published) and draft_changed tells whether the draft differs from it.

NameInTypeDescription
id
required
path string Agent id, e.g. front-desk.

Request

curl
curl "https://hubvocal.com/v1/agents/front-desk" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
{
  "id": "front-desk",
  "name": "Front desk",
  "lang": "en",
  "engine": "live",
  "instructions": "You are the front desk assistant for a dental clinic. Help callers book appointments and answer general questions briefly. Never make things up; offer a callback from the team instead.",
  "greeting": "Hello, thanks for calling the clinic. How can I help you today?",
  "voice": {
    "voice": "marin",
    "speed": 1
  },
  "tools": {
    "builtins": [
      "end_call",
      "take_message"
    ],
    "webhooks": [
      "tl_8f3a2c"
    ]
  },
  "turn": {
    "first_speaker": "agent",
    "idle_check_s": 30
  },
  "recording": {
    "enabled": false,
    "announce": true
  },
  "post_call": {
    "summary": true,
    "fields": [
      {
        "name": "appointment_date",
        "type": "date",
        "description": "Requested appointment date, if any"
      },
      {
        "name": "callback",
        "type": "boolean",
        "description": "Does the caller want a callback?"
      }
    ]
  },
  "knowledge": {
    "doc_ids": [
      "kb_7d21e4"
    ],
    "mode": "tool"
  },
  "published_version": 3,
  "draft_changed": false
}

Errors

  • 404 No agent with this id.

Update an agent

PUT /v1/agents/{id}

Replaces the agent with the object you send. Send the full object: fields you leave out fall back to their defaults. The id in the path wins over any id in the body.

NameInTypeDescription
id
required
path string Agent id, e.g. front-desk.
name
required
body string Display name, up to 60 characters.
lang
required
body string Conversation language code, e.g. tr, en, de. The full list is in GET /v1/voices.
engine
required
body "live" | "gemini" | "realtime" | "classic" live: GPT-Live 1, gemini: Gemini Live, realtime: OpenAI Realtime, classic: STT → LLM → TTS.
instructions body string System instructions, up to 8000 characters.
greeting body string What the agent says first, up to 400 characters.
voice body object voice (a name from /v1/voices), speed (classic and realtime only; fixed at 1.0 for live and gemini), tone (free-text speaking style, up to 600 characters).
tools body object builtins (end_call, take_message) and webhooks (tool ids); up to 16 tools in total. Empty or missing means no tools. mcp: [{server_id, tools}] adds tools from your MCP servers; they count towards the same limit.
turn body object Turn-taking settings: sensitivity (low, balanced, high; classic engine only), first_speaker (agent or caller), silence_wait_s (1–5), idle_check_s (0 or 10–120), agent_timeout_s (0 or 10–60). Empty fields use the defaults.
recording body object recording: {enabled (default false), announce (default true), announce_text?}. When on, the agent announces the recording before the greeting.
post_call body object post_call.summary: write a summary when the call ends (default true). post_call.fields: up to 24 fields to fill after the call ({name, type, description, enum?, required?}).
knowledge body object doc_ids (up to 50 knowledge base documents) and mode ("tool"). The agent then gets the search_knowledge tool.
calendar body object integration_id and calendar_id from the dashboard, duration_min (15–240), buffer_min (0–60), timezone, working_hours, min_notice_h (0–72), max_days_ahead (1–90). Enables the appointment tools.

Request

curl
curl -X PUT "https://hubvocal.com/v1/agents/front-desk" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Front desk",
  "lang": "en",
  "engine": "live",
  "instructions": "You are the front desk assistant for a dental clinic. Help callers book appointments and answer general questions briefly. Never make things up; offer a callback from the team instead.",
  "greeting": "Hello, thanks for calling the clinic. How can I help you today?",
  "voice": {
    "voice": "cedar",
    "speed": 1
  },
  "tools": {
    "builtins": [
      "end_call",
      "take_message"
    ],
    "webhooks": [
      "tl_8f3a2c"
    ]
  },
  "turn": {
    "first_speaker": "agent",
    "idle_check_s": 30
  },
  "recording": {
    "enabled": false,
    "announce": true
  },
  "post_call": {
    "summary": true,
    "fields": [
      {
        "name": "appointment_date",
        "type": "date",
        "description": "Requested appointment date, if any"
      },
      {
        "name": "callback",
        "type": "boolean",
        "description": "Does the caller want a callback?"
      }
    ]
  },
  "knowledge": {
    "doc_ids": [
      "kb_7d21e4"
    ],
    "mode": "tool"
  }
}'

Response · 200

JSON
{
  "id": "front-desk",
  "name": "Front desk",
  "lang": "en",
  "engine": "live",
  "instructions": "You are the front desk assistant for a dental clinic. Help callers book appointments and answer general questions briefly. Never make things up; offer a callback from the team instead.",
  "greeting": "Hello, thanks for calling the clinic. How can I help you today?",
  "voice": {
    "voice": "cedar",
    "speed": 1
  },
  "tools": {
    "builtins": [
      "end_call",
      "take_message"
    ],
    "webhooks": [
      "tl_8f3a2c"
    ]
  },
  "turn": {
    "first_speaker": "agent",
    "idle_check_s": 30
  },
  "recording": {
    "enabled": false,
    "announce": true
  },
  "post_call": {
    "summary": true,
    "fields": [
      {
        "name": "appointment_date",
        "type": "date",
        "description": "Requested appointment date, if any"
      },
      {
        "name": "callback",
        "type": "boolean",
        "description": "Does the caller want a callback?"
      }
    ]
  },
  "knowledge": {
    "doc_ids": [
      "kb_7d21e4"
    ],
    "mode": "tool"
  },
  "published_version": 3,
  "draft_changed": true
}

Errors

  • 404 No agent with this id. Create it with POST first.

Delete an agent

DELETE /v1/agents/{id}

Deletes the agent. An organization always keeps at least one agent, so deleting the last one fails.

NameInTypeDescription
id
required
path string Agent id, e.g. front-desk.

Request

curl
curl -X DELETE "https://hubvocal.com/v1/agents/front-desk" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
{
  "deleted": "front-desk"
}

Errors

  • 400 This is the last agent in the organization.
  • 404 No agent with this id.

List versions

GET /v1/agents/{id}/versions

The agent's published versions, newest first. is_live marks the one calls use now.

NameInTypeDescription
id
required
path string Agent id, e.g. front-desk.

Request

curl
curl "https://hubvocal.com/v1/agents/front-desk/versions" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
[
  {
    "version": 3,
    "note": "Shorter greeting",
    "published_at": 1793829000,
    "published_by": "ayse@example.com",
    "is_live": true
  },
  {
    "version": 2,
    "note": "Added price list",
    "published_at": 1793740000,
    "published_by": "ayse@example.com",
    "is_live": false
  }
]

Errors

  • 404 No agent with this id.

Retrieve a version

GET /v1/agents/{id}/versions/{n}

The full agent as it was in that version.

NameInTypeDescription
id
required
path string Agent id, e.g. front-desk.
n
required
path integer Version number.

Request

curl
curl "https://hubvocal.com/v1/agents/front-desk/versions/2" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
{
  "id": "front-desk",
  "name": "Front desk",
  "lang": "en",
  "engine": "live",
  "instructions": "You are the front desk assistant for a dental clinic. Help callers book appointments and answer general questions briefly. Never make things up; offer a callback from the team instead.",
  "greeting": "Hello, thanks for calling the clinic. How can I help you today?",
  "voice": {
    "voice": "marin",
    "speed": 1
  },
  "tools": {
    "builtins": [
      "end_call",
      "take_message"
    ],
    "webhooks": [
      "tl_8f3a2c"
    ]
  },
  "turn": {
    "first_speaker": "agent",
    "idle_check_s": 30
  },
  "recording": {
    "enabled": false,
    "announce": true
  },
  "post_call": {
    "summary": true,
    "fields": [
      {
        "name": "appointment_date",
        "type": "date",
        "description": "Requested appointment date, if any"
      },
      {
        "name": "callback",
        "type": "boolean",
        "description": "Does the caller want a callback?"
      }
    ]
  },
  "knowledge": {
    "doc_ids": [
      "kb_7d21e4"
    ],
    "mode": "tool"
  },
  "version": 2
}

Errors

  • 404 No such agent or version.

Publish the draft

POST /v1/agents/{id}/publish

Saves the current draft as a new version and makes it live. Calls that are already running keep their version.

NameInTypeDescription
id
required
path string Agent id, e.g. front-desk.
note body string A short note about what changed.

Request

curl
curl -X POST "https://hubvocal.com/v1/agents/front-desk/publish" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "note": "Shorter greeting"
}'

Response · 200

JSON
{
  "version": 4
}

Errors

  • 404 No agent with this id.

Roll back

POST /v1/agents/{id}/rollback

Copies an earlier version into the draft and publishes it as a new version.

NameInTypeDescription
id
required
path string Agent id, e.g. front-desk.
version
required
body integer The version to bring back.

Request

curl
curl -X POST "https://hubvocal.com/v1/agents/front-desk/rollback" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "version": 2
}'

Response · 200

JSON
{
  "version": 5
}

Errors

  • 404 No such agent or version.

Knowledge base

List documents

GET /v1/knowledge

Your organization's documents with their processing status, size, number of chunks and the agents that use them.

Request

curl
curl "https://hubvocal.com/v1/knowledge" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
[
  {
    "id": "kb_7d21e4",
    "title": "Price list 2026",
    "kind": "file",
    "filename": "price-list-2026.pdf",
    "url": null,
    "size_bytes": 248113,
    "chunks": 42,
    "status": "ready",
    "error": null,
    "created_at": 1793740000,
    "used_by": [
      "front-desk"
    ]
  }
]

Add a document

POST /v1/knowledge

Upload a file as multipart/form-data (field file), send text as JSON {title, text} or a single web page as JSON {url}. Returns 201 with status processing; poll the document until it is ready or failed.

NameInTypeDescription
file body file multipart/form-data: pdf, docx, txt, md or html, up to 10 MB.
title body string Title, with text.
text body string Plain text content.
url body string A single https page to fetch instead.

Request

curl
curl -X POST "https://hubvocal.com/v1/knowledge" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "title": "Opening hours",
  "text": "We are open 09:00–18:00 on weekdays and 10:00–14:00 on Saturdays. Closed on Sundays."
}'

Response · 201

JSON
{
  "id": "kb_9f03b1",
  "title": "Opening hours",
  "kind": "text",
  "filename": null,
  "url": null,
  "size_bytes": 96,
  "chunks": 0,
  "status": "processing",
  "error": null,
  "created_at": 1793740000,
  "used_by": []
}

Errors

  • 400 Unsupported file type, file over 10 MB, URL not https or pointing to a private address, or the organization limit (200 documents, 200 MB) is reached.

Retrieve a document

GET /v1/knowledge/{id}

The document with a preview of its first 2000 characters.

NameInTypeDescription
id
required
path string Document id.

Request

curl
curl "https://hubvocal.com/v1/knowledge/kb_7d21e4" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
{
  "id": "kb_7d21e4",
  "title": "Price list 2026",
  "kind": "file",
  "filename": "price-list-2026.pdf",
  "url": null,
  "size_bytes": 248113,
  "chunks": 42,
  "status": "ready",
  "error": null,
  "created_at": 1793740000,
  "used_by": [
    "front-desk"
  ],
  "preview": "Price list 2026. Check-up 1,500 TRY, scale and polish 2,000 TRY…"
}

Errors

  • 404 No document with this id.

Delete a document

DELETE /v1/knowledge/{id}

Deletes the document and removes it from every agent that used it.

NameInTypeDescription
id
required
path string Document id.

Request

curl
curl -X DELETE "https://hubvocal.com/v1/knowledge/kb_7d21e4" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
{
  "deleted": "kb_7d21e4"
}

Errors

  • 404 No document with this id.

Search

POST /v1/knowledge/search

Runs the same search the agent uses and returns the best matching chunks with a score. Useful to check your documents before going live.

NameInTypeDescription
query
required
body string What to search for.
doc_ids body string[] Limit the search to these documents.

Request

curl
curl -X POST "https://hubvocal.com/v1/knowledge/search" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "query": "how much is a scale and polish"
}'

Response · 200

JSON
[
  {
    "doc_id": "kb_7d21e4",
    "title": "Price list 2026",
    "chunk_index": 3,
    "text": "Scale and polish: 2,000 TRY, about 30 minutes.",
    "score": 0.82
  }
]

Tools

Built-in tools

These tools need no definition. Turn them on in the agent's tools.builtins. search_knowledge is added when knowledge is set. The appointment tools are added when calendar is set.

NameWhat it does
end_callThe agent says goodbye and hangs up 0.75 s after it stops speaking. If the caller speaks in the meantime, hanging up is cancelled. The call record shows ended_by: "agent".
take_messageTakes name, phone, message and callback_requested and writes them to outcomes[] in the call record. It does not ask for confirmation.
search_knowledgeAdded automatically when the agent has documents in knowledge. Searches them and records the sources in the call record.
check_availabilityRead. Returns free slots between date_from and date_to, within the agent's working hours. Added when calendar is set.
book_appointmentWrite, behind the confirmation step. Books start for name and phone, with an optional note. The booking goes to outcomes[].
cancel_appointmentWrite, behind the confirmation step. Cancels a booking by its booking_ref.

Calendar integrations (Google Calendar) are connected in the dashboard only; there is no /v1 endpoint for them. Use the integration and calendar ids from the dashboard in the agent's calendar field.

List tools

GET /v1/tools

Your organization's webhook tools. The secret is never returned; has_secret shows whether one is set.

Request

curl
curl "https://hubvocal.com/v1/tools" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
[
  {
    "id": "tl_8f3a2c",
    "name": "check_order",
    "description": "Looks up the shipping status of an order by its number.",
    "kind": "read",
    "url": "https://crm.example.com/hooks/order",
    "parameters": {
      "type": "object",
      "properties": {
        "order_id": {
          "type": "string",
          "description": "Order number"
        }
      },
      "required": [
        "order_id"
      ]
    },
    "timeout_s": 8,
    "enabled": true,
    "has_secret": true,
    "created_at": 1793830000,
    "updated_at": 1793830000
  }
]

Retrieve a tool

GET /v1/tools/{id}

A single webhook tool. The secret is never returned.

NameInTypeDescription
id
required
path string Tool id.

Request

curl
curl "https://hubvocal.com/v1/tools/tl_8f3a2c" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
{
  "id": "tl_8f3a2c",
  "name": "check_order",
  "description": "Looks up the shipping status of an order by its number.",
  "kind": "read",
  "url": "https://crm.example.com/hooks/order",
  "parameters": {
    "type": "object",
    "properties": {
      "order_id": {
        "type": "string",
        "description": "Order number"
      }
    },
    "required": [
      "order_id"
    ]
  },
  "timeout_s": 8,
  "enabled": true,
  "has_secret": true,
  "created_at": 1793830000,
  "updated_at": 1793830000
}

Errors

  • 404 No tool with this id.

Create a tool

POST /v1/tools

Defines a webhook tool for the organization. Add its id to an agent's tools.webhooks to use it. The URL must be https and may not point to a local or private address.

NameInTypeDescription
name
required
body string Lowercase letters, digits and underscores, starting with a letter (3–41 characters). Unique in the organization; end_call and take_message are reserved.
description
required
body string Tells the model when and how to use the tool.
kind
required
body "read" | "write" read runs immediately; write asks the caller to confirm first.
url
required
body string Your endpoint. https only; local and private addresses are rejected.
parameters body object JSON Schema object: up to 20 fields of type string, number, integer or boolean (enum allowed).
timeout_s body integer 2 to 15 seconds.
enabled body boolean Disabled tools are not offered to the model.
secret body string Used to sign requests. Write-only; on update, empty keeps the current secret and "-" removes it.

Request

curl
curl -X POST "https://hubvocal.com/v1/tools" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "check_order",
  "description": "Looks up the shipping status of an order by its number.",
  "kind": "read",
  "url": "https://crm.example.com/hooks/order",
  "parameters": {
    "type": "object",
    "properties": {
      "order_id": {
        "type": "string",
        "description": "Order number"
      }
    },
    "required": [
      "order_id"
    ]
  },
  "timeout_s": 8,
  "enabled": true,
  "secret": "whsec_2b7f…"
}'

Response · 201

JSON
{
  "id": "tl_8f3a2c",
  "name": "check_order",
  "description": "Looks up the shipping status of an order by its number.",
  "kind": "read",
  "url": "https://crm.example.com/hooks/order",
  "parameters": {
    "type": "object",
    "properties": {
      "order_id": {
        "type": "string",
        "description": "Order number"
      }
    },
    "required": [
      "order_id"
    ]
  },
  "timeout_s": 8,
  "enabled": true,
  "has_secret": true,
  "created_at": 1793830000,
  "updated_at": 1793830000
}

Errors

  • 400 A field breaks the rules: name format, https URL, private address, more than 20 parameters or timeout out of range.
  • 409 A tool with this name already exists, or the name is reserved.

Update a tool

PUT /v1/tools/{id}

Replaces the tool with the object you send. Leave secret empty or out to keep the current one; send "-" to remove it.

NameInTypeDescription
id
required
path string Tool id.
name
required
body string Lowercase letters, digits and underscores, starting with a letter (3–41 characters). Unique in the organization; end_call and take_message are reserved.
description
required
body string Tells the model when and how to use the tool.
kind
required
body "read" | "write" read runs immediately; write asks the caller to confirm first.
url
required
body string Your endpoint. https only; local and private addresses are rejected.
parameters body object JSON Schema object: up to 20 fields of type string, number, integer or boolean (enum allowed).
timeout_s body integer 2 to 15 seconds.
enabled body boolean Disabled tools are not offered to the model.
secret body string Used to sign requests. Write-only; on update, empty keeps the current secret and "-" removes it.

Request

curl
curl -X PUT "https://hubvocal.com/v1/tools/tl_8f3a2c" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "check_order",
  "description": "Looks up the shipping status of an order by its number.",
  "kind": "read",
  "url": "https://crm.example.com/hooks/order",
  "parameters": {
    "type": "object",
    "properties": {
      "order_id": {
        "type": "string",
        "description": "Order number"
      }
    },
    "required": [
      "order_id"
    ]
  },
  "timeout_s": 10,
  "enabled": true,
  "secret": ""
}'

Response · 200

JSON
{
  "id": "tl_8f3a2c",
  "name": "check_order",
  "description": "Looks up the shipping status of an order by its number.",
  "kind": "read",
  "url": "https://crm.example.com/hooks/order",
  "parameters": {
    "type": "object",
    "properties": {
      "order_id": {
        "type": "string",
        "description": "Order number"
      }
    },
    "required": [
      "order_id"
    ]
  },
  "timeout_s": 10,
  "enabled": true,
  "has_secret": true,
  "created_at": 1793830000,
  "updated_at": 1793833600
}

Errors

  • 400 A field breaks the rules: name format, https URL, private address, more than 20 parameters or timeout out of range.
  • 404 No tool with this id.

Delete a tool

DELETE /v1/tools/{id}

Deletes the tool and removes it from every agent that used it.

NameInTypeDescription
id
required
path string Tool id.

Request

curl
curl -X DELETE "https://hubvocal.com/v1/tools/tl_8f3a2c" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
{
  "deleted": "tl_8f3a2c"
}

Errors

  • 404 No tool with this id.

Test a tool

POST /v1/tools/{id}/test

Sends a real request to the tool's URL with the arguments you give and returns the status, the time it took and up to 4 KB of the response body.

NameInTypeDescription
id
required
path string Tool id.
arguments body object Sample arguments, matching parameters.

Request

curl
curl -X POST "https://hubvocal.com/v1/tools/tl_8f3a2c/test" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "arguments": {
    "order_id": "A-10234"
  }
}'

Response · 200

JSON
{
  "ok": true,
  "status": 200,
  "ms": 412,
  "body": "{\"order_id\":\"A-10234\",\"status\":\"shipped\",\"eta\":\"2026-11-07\"}"
}

Errors

  • 404 No tool with this id.

MCP servers

List MCP servers

GET /v1/mcp-servers

Your organization's MCP servers with their status and synced tools. auth_set shows whether a secret is stored; it is never returned.

Request

curl
curl "https://hubvocal.com/v1/mcp-servers" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
[
  {
    "id": "mcp_2b8e41",
    "name": "crm",
    "url": "https://mcp.example.com/mcp",
    "transport": "streamable_http",
    "auth": "bearer",
    "auth_set": true,
    "enabled": true,
    "status": "ok",
    "error": null,
    "tools_synced_at": 1793831000,
    "tools": [
      {
        "name": "find_customer",
        "description": "Find a customer by phone number.",
        "input_schema": {
          "type": "object",
          "properties": {
            "phone": {
              "type": "string"
            }
          },
          "required": [
            "phone"
          ]
        },
        "enabled": true,
        "access": "read"
      },
      {
        "name": "create_ticket",
        "description": "Open a support ticket.",
        "input_schema": {
          "type": "object",
          "properties": {
            "subject": {
              "type": "string"
            },
            "body": {
              "type": "string"
            }
          },
          "required": [
            "subject"
          ]
        },
        "enabled": false,
        "access": "write"
      }
    ]
  }
]

Add an MCP server

POST /v1/mcp-servers

Saves the server and fetches its tool list. New tools are switched off. Up to 10 servers per organization.

NameInTypeDescription
name
required
body string Short name; used as the prefix in tool names (<name>.<tool>).
url
required
body string The server's MCP endpoint. https only; local and private addresses are rejected.
transport body string streamable_http (default) or sse.
auth body string none, bearer or header.
secret body string Token or header value. Stored encrypted, never returned; "-" removes it.
header_name body string Header name when auth is header.

Request

curl
curl -X POST "https://hubvocal.com/v1/mcp-servers" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "crm",
  "url": "https://mcp.example.com/mcp",
  "auth": "bearer",
  "secret": "sk_live_…"
}'

Response · 201

JSON
{
  "id": "mcp_2b8e41",
  "name": "crm",
  "url": "https://mcp.example.com/mcp",
  "transport": "streamable_http",
  "auth": "bearer",
  "auth_set": true,
  "enabled": true,
  "status": "ok",
  "error": null,
  "tools_synced_at": 1793831000,
  "tools": [
    {
      "name": "find_customer",
      "description": "Find a customer by phone number.",
      "input_schema": {
        "type": "object",
        "properties": {
          "phone": {
            "type": "string"
          }
        },
        "required": [
          "phone"
        ]
      },
      "enabled": false,
      "access": "write"
    },
    {
      "name": "create_ticket",
      "description": "Open a support ticket.",
      "input_schema": {
        "type": "object",
        "properties": {
          "subject": {
            "type": "string"
          },
          "body": {
            "type": "string"
          }
        },
        "required": [
          "subject"
        ]
      },
      "enabled": false,
      "access": "write"
    }
  ]
}

Errors

  • 400 The URL is not https or points to a private address, or the server's tool list could not be fetched.
  • 409 The organization already has 10 MCP servers.

Update an MCP server

PUT /v1/mcp-servers/{id}

Changes the name, URL, authentication or enabled state, and which tools are on and whether they are read or write. Send secret: "-" to remove the secret.

NameInTypeDescription
id
required
path string MCP server id.
name body string Short name; used as the prefix in tool names (<name>.<tool>).
url body string The server's MCP endpoint. https only; local and private addresses are rejected.
enabled body boolean A disabled server is not offered to agents.
auth body string none, bearer or header.
secret body string Token or header value. Stored encrypted, never returned; "-" removes it.
tools body object[] [{name, enabled, access}] per tool; access is read or write.

Request

curl
curl -X PUT "https://hubvocal.com/v1/mcp-servers/mcp_2b8e41" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "tools": [
    {
      "name": "find_customer",
      "enabled": true,
      "access": "read"
    },
    {
      "name": "create_ticket",
      "enabled": true,
      "access": "write"
    }
  ]
}'

Response · 200

JSON
{
  "id": "mcp_2b8e41",
  "name": "crm",
  "url": "https://mcp.example.com/mcp",
  "transport": "streamable_http",
  "auth": "bearer",
  "auth_set": true,
  "enabled": true,
  "status": "ok",
  "error": null,
  "tools_synced_at": 1793831000,
  "tools": [
    {
      "name": "find_customer",
      "description": "Find a customer by phone number.",
      "input_schema": {
        "type": "object",
        "properties": {
          "phone": {
            "type": "string"
          }
        },
        "required": [
          "phone"
        ]
      },
      "enabled": true,
      "access": "read"
    },
    {
      "name": "create_ticket",
      "description": "Open a support ticket.",
      "input_schema": {
        "type": "object",
        "properties": {
          "subject": {
            "type": "string"
          },
          "body": {
            "type": "string"
          }
        },
        "required": [
          "subject"
        ]
      },
      "enabled": true,
      "access": "write"
    }
  ]
}

Errors

  • 400 The URL is not https or points to a private address, or the server's tool list could not be fetched.
  • 404 No MCP server with this id.

Delete an MCP server

DELETE /v1/mcp-servers/{id}

Deletes the server and removes its tools from every agent.

NameInTypeDescription
id
required
path string MCP server id.

Request

curl
curl -X DELETE "https://hubvocal.com/v1/mcp-servers/mcp_2b8e41" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
{
  "deleted": "mcp_2b8e41"
}

Errors

  • 404 No MCP server with this id.

Sync tools

POST /v1/mcp-servers/{id}/sync

Fetches the tool list again. Tools that are new arrive switched off, with access set to write.

NameInTypeDescription
id
required
path string MCP server id.

Request

curl
curl -X POST "https://hubvocal.com/v1/mcp-servers/mcp_2b8e41/sync" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
{
  "id": "mcp_2b8e41",
  "name": "crm",
  "url": "https://mcp.example.com/mcp",
  "transport": "streamable_http",
  "auth": "bearer",
  "auth_set": true,
  "enabled": true,
  "status": "ok",
  "error": null,
  "tools_synced_at": 1793831000,
  "tools": [
    {
      "name": "find_customer",
      "description": "Find a customer by phone number.",
      "input_schema": {
        "type": "object",
        "properties": {
          "phone": {
            "type": "string"
          }
        },
        "required": [
          "phone"
        ]
      },
      "enabled": true,
      "access": "read"
    },
    {
      "name": "create_ticket",
      "description": "Open a support ticket.",
      "input_schema": {
        "type": "object",
        "properties": {
          "subject": {
            "type": "string"
          },
          "body": {
            "type": "string"
          }
        },
        "required": [
          "subject"
        ]
      },
      "enabled": false,
      "access": "write"
    }
  ]
}

Errors

  • 400 The URL is not https or points to a private address, or the server's tool list could not be fetched.
  • 404 No MCP server with this id.

Test a tool

POST /v1/mcp-servers/{id}/test

Calls one tool on the server with the arguments you give and returns the time it took, up to 4 KB of content and whether the tool reported an error.

NameInTypeDescription
id
required
path string MCP server id.
tool
required
body string Name of the tool to call.
arguments body object Arguments for the tool, matching its input_schema.

Request

curl
curl -X POST "https://hubvocal.com/v1/mcp-servers/mcp_2b8e41/test" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "tool": "find_customer",
  "arguments": {
    "phone": "+905321234567"
  }
}'

Response · 200

JSON
{
  "ok": true,
  "ms": 236,
  "content": "{\"customer_id\":\"C-1042\",\"name\":\"Ayşe Yılmaz\",\"open_tickets\":0}",
  "is_error": false
}

Errors

  • 404 No MCP server with this id.

Webhooks

List webhooks

GET /v1/webhooks

Your organization's webhooks with the result of their last delivery. The secret is never returned here; secret_set shows whether one is set.

Request

curl
curl "https://hubvocal.com/v1/webhooks" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
[
  {
    "id": "wh_3c9d1e",
    "url": "https://crm.example.com/hooks/voice",
    "events": [
      "call.ended"
    ],
    "enabled": true,
    "secret_set": true,
    "created_at": 1793830000,
    "last_delivery": {
      "status": "ok",
      "at": 1793832440.2,
      "http_status": 200
    }
  }
]

Create a webhook

POST /v1/webhooks

Subscribes a URL to events. Currently the only event is call.ended. Up to 5 webhooks per organization. If you leave out secret, one is generated and returned only in this response.

NameInTypeDescription
url
required
body string Your endpoint. https only; local and private addresses are rejected.
events
required
body string[] Events to receive. Currently ["call.ended"].
enabled body boolean Disabled webhooks receive nothing.
secret body string Signing secret. Generated when left out; returned only once.

Request

curl
curl -X POST "https://hubvocal.com/v1/webhooks" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://crm.example.com/hooks/voice",
  "events": [
    "call.ended"
  ]
}'

Response · 201

JSON
{
  "id": "wh_3c9d1e",
  "url": "https://crm.example.com/hooks/voice",
  "events": [
    "call.ended"
  ],
  "enabled": true,
  "secret_set": true,
  "created_at": 1793830000,
  "last_delivery": null,
  "secret": "whsec_9QmZ…"
}

Errors

  • 400 The URL is not https, points to a private address, or an event is unknown.
  • 409 The organization already has 5 webhooks.

Update a webhook

PUT /v1/webhooks/{id}

Changes the URL, events or enabled state. Send secret: "-" to generate a new secret; it is returned in the response.

NameInTypeDescription
id
required
path string Webhook id.
url body string Your endpoint. https only; local and private addresses are rejected.
events body string[] Events to receive. Currently ["call.ended"].
enabled body boolean Disabled webhooks receive nothing.
secret body string "-" generates a new secret. Leave out to keep the current one.

Request

curl
curl -X PUT "https://hubvocal.com/v1/webhooks/wh_3c9d1e" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "enabled": false
}'

Response · 200

JSON
{
  "id": "wh_3c9d1e",
  "url": "https://crm.example.com/hooks/voice",
  "events": [
    "call.ended"
  ],
  "enabled": false,
  "secret_set": true,
  "created_at": 1793830000,
  "last_delivery": {
    "status": "ok",
    "at": 1793832440.2,
    "http_status": 200
  }
}

Errors

  • 400 The URL is not https, points to a private address, or an event is unknown.
  • 404 No webhook with this id.

Delete a webhook

DELETE /v1/webhooks/{id}

Deletes the webhook. Pending deliveries are dropped.

NameInTypeDescription
id
required
path string Webhook id.

Request

curl
curl -X DELETE "https://hubvocal.com/v1/webhooks/wh_3c9d1e" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
{
  "deleted": "wh_3c9d1e"
}

Errors

  • 404 No webhook with this id.

Test a webhook

POST /v1/webhooks/{id}/test

Sends a sample call.ended event to the URL and returns the status, the time it took and the response body.

NameInTypeDescription
id
required
path string Webhook id.

Request

curl
curl -X POST "https://hubvocal.com/v1/webhooks/wh_3c9d1e/test" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
{
  "ok": true,
  "http_status": 200,
  "ms": 184,
  "body": "{\"received\":true}"
}

Errors

  • 404 No webhook with this id.

List deliveries

GET /v1/webhooks/{id}/deliveries

The last 50 deliveries of a webhook, newest first, with status (pending, ok, failed), number of attempts, HTTP status, error and the time of the next attempt.

NameInTypeDescription
id
required
path string Webhook id.

Request

curl
curl "https://hubvocal.com/v1/webhooks/wh_3c9d1e/deliveries" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
[
  {
    "id": "dl_51b7a9",
    "event": "call.ended",
    "call_id": "demo-8585522d",
    "status": "ok",
    "attempts": 1,
    "http_status": 200,
    "error": null,
    "created_at": 1793832440.2,
    "next_attempt_at": null
  },
  {
    "id": "dl_51a3c4",
    "event": "call.ended",
    "call_id": "tel-6b1f04d2",
    "status": "pending",
    "attempts": 2,
    "http_status": 503,
    "error": "HTTP 503",
    "created_at": 1793832101.7,
    "next_attempt_at": 1793833901.7
  }
]

Errors

  • 404 No webhook with this id.

Retry a delivery

POST /v1/webhooks/deliveries/{id}/retry

Puts a failed delivery back in the queue.

NameInTypeDescription
id
required
path string Delivery id from the deliveries list.

Request

curl
curl -X POST "https://hubvocal.com/v1/webhooks/deliveries/dl_51a3c4/retry" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
{
  "ok": true
}

Errors

  • 404 No delivery with this id, or it has not failed.

Voices and engines

List engines, voices and limits

GET /v1/voices

Everything you need to build an agent form: engine names, valid voices per engine, speed ranges, languages, tone presets, the model behind each engine and field length limits. The example below is shortened.

Request

curl
curl "https://hubvocal.com/v1/voices" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
{
  "engines": {
    "live": "GPT-Live 1 (OpenAI full-duplex ses modeli)",
    "gemini": "Gemini Live (Google ses-sese model)",
    "realtime": "Realtime (OpenAI ses-sese model)",
    "classic": "Klasik (Deepgram + OpenAI LLM + OpenAI TTS)"
  },
  "voices": {
    "live": [
      "marin",
      "cedar",
      "gleam",
      "meridian"
    ],
    "gemini": [
      "Kore",
      "Aoede",
      "Charon",
      "Puck"
    ],
    "realtime": [
      "marin",
      "cedar",
      "alloy"
    ],
    "classic": [
      "alloy",
      "coral",
      "sage"
    ]
  },
  "speed": {
    "live": {
      "min": 1,
      "max": 1
    },
    "gemini": {
      "min": 1,
      "max": 1
    },
    "realtime": {
      "min": 0.25,
      "max": 1.5
    },
    "classic": {
      "min": 0.25,
      "max": 4
    }
  },
  "langs": {
    "tr": "Türkçe",
    "en": "English"
  },
  "presets": [
    {
      "id": "calm",
      "label": "Sakin ve güven veren",
      "text": "Sakin, yumuşak ve güven veren bir tonla konuş."
    }
  ],
  "models": {
    "live": {
      "model": "gpt-live-1",
      "backend": "gpt-5.6-luna"
    },
    "gemini": {
      "model": "gemini-3.8-live",
      "ready": true
    },
    "realtime": {
      "model": "gpt-realtime-2.1-mini"
    },
    "classic": {
      "stt": "nova-3",
      "llm": "gpt-4.1-mini",
      "tts": "gpt-4o-mini-tts"
    }
  },
  "limits": {
    "tone": 600,
    "instructions": 8000,
    "greeting": 400
  }
}

Calls

Retention is set per organization in the dashboard: recordings_days (default 30), transcripts_days (default 365) and pii_redaction. A daily job deletes expired recordings and transcripts; the call row and its cost remain.

Web widget: public keys (pk_…) are created per agent in the dashboard. The widget script calls POST /api/public/web-calls; see the guide on the Developers page.

Start a web call

POST /v1/web-calls

Opens a room for a browser call and returns a short-lived access token. Pass server_url and token to livekit-client in the browser; the agent joins the room on its own. Call this from your server so the API key never reaches the browser. Instead of agent_id you can send a full agent object to test a draft without saving it.

NameInTypeDescription
agent_id body string Id of a saved agent. Required unless you send agent.
agent body object A full agent object (same fields as POST /v1/agents) used for this call only.
version body string | integer Which version to use: "live" (default), "draft" or a version number.

Request

curl
curl -X POST "https://hubvocal.com/v1/web-calls" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "agent_id": "front-desk"
}'

Response · 200

JSON
{
  "server_url": "wss://hubvocal.com",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
  "room": "demo-3f9a1c2e",
  "max_seconds": 300
}

Errors

  • 400 Neither agent_id nor agent was sent.
  • 402 The organization has reached its monthly spending limit.
  • 403 The organization is suspended.
  • 404 No agent with this id.

Place an outbound phone call

POST /v1/calls

Dials a phone number from one of your organization's numbers and connects the agent when the call is answered. The provider (DIDWW, Twilio or your own SIP trunk) is picked from the from number.

NameInTypeDescription
from
required
body string One of your organization's numbers, in E.164 format (+90…).
to
required
body string The number to call, in E.164 format.
agent_id body string Agent to use. When omitted, the agent assigned to the from number answers.
version body string | integer Which version to use: "live" (default), "draft" or a version number.

Request

curl
curl -X POST "https://hubvocal.com/v1/calls" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "from": "+908501234567",
  "to": "+905321234567",
  "agent_id": "front-desk"
}'

Response · 200

JSON
{
  "room": "tel-out-9c41e0b7",
  "provider": "didww",
  "sip_call_id": "SCL_7mVt2kQb9xRa"
}

Errors

  • 400 A number is not in international format, or from is not one of your numbers.
  • 402 The organization has reached its monthly spending limit.
  • 502 The telephony provider rejected the call.

List calls

GET /v1/calls

Calls from newest to oldest, web and phone. quality is the browser-side measurement summary and is null for phone calls. ended_by is caller, agent, max_call_seconds, caller_unresponsive, agent_unresponsive, no_answer or unknown. channel is web, phone_in or phone_out. Each row also has outcome and summary_text (the first 160 characters of the summary). ended_by is supervisor when a supervisor ended the call from the live console.

NameInTypeDescription
limit query integer Default 50, maximum 500.
agent query string Only calls of this agent (agent name).
outcome query string Only calls with this outcome.

Request

curl
curl "https://hubvocal.com/v1/calls?limit=20" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
[
  {
    "room": "demo-8585522d",
    "org_id": "6f1c2a40-3b7e-4d0c-9a51-2f8e7c1d9b03",
    "org_name": "Example Clinic",
    "channel": "web",
    "profile": "Front desk",
    "engine": "live",
    "lang": "en",
    "voice": "marin",
    "started_at": 1793832409.1,
    "ended_at": 1793832434.2,
    "duration_s": 25.1,
    "total_usd": 0.021215,
    "ended_by": "caller",
    "outcome": "message_taken",
    "summary_text": "The caller asked for an appointment on Thursday afternoon; a message was taken for a callback.",
    "extracted": {
      "appointment_date": "2026-11-12",
      "callback": true
    },
    "first_audio_s": 2.43,
    "turns": 4,
    "source_gaps": {
      "count": 0,
      "total_ms": 0,
      "max_ms": 0
    },
    "quality": {
      "verdict": "Clean: no loss, no gaps.",
      "protocol": "UDP",
      "loss_pct": 0,
      "conceal_pct": 0,
      "jitter_ms": 4,
      "rtt_ms": 38
    }
  }
]

Retrieve a call

GET /v1/calls/{room}

The full call record: cost breakdown per provider, transcript, greeting timing, agent-side source gaps and the browser quality timeline in 2-second windows. Cost fields depend on the engine (live, llm, stt, tts, realtime…); each one carries usd. source_gaps.events lists [second, ms] pairs. summary holds text, outcome (resolved, message_taken, transferred, unresolved, no_conversation, spam), sentiment, topics and action_items. supervisors lists who joined the call from the live console and in which modes. recording holds available, duration_s and size_bytes. extracted holds the values of the agent's post_call.fields (null when not mentioned).

NameInTypeDescription
room
required
path string Room name from the call list or from POST /v1/web-calls.

Request

curl
curl "https://hubvocal.com/v1/calls/demo-8585522d" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
{
  "call": {
    "room": "demo-8585522d",
    "org_id": "6f1c2a40-3b7e-4d0c-9a51-2f8e7c1d9b03",
    "org_name": "Example Clinic",
    "channel": "web",
    "profile": "Front desk",
    "engine": "live",
    "lang": "en",
    "voice": "marin",
    "started_at": 1793832409.1,
    "ended_at": 1793832434.2,
    "duration_s": 25.1,
    "total_usd": 0.021215,
    "ended_by": "caller",
    "outcome": "message_taken",
    "summary_text": "The caller asked for an appointment on Thursday afternoon; a message was taken for a callback.",
    "extracted": {
      "appointment_date": "2026-11-12",
      "callback": true
    },
    "first_audio_s": 2.43,
    "turns": 4,
    "source_gaps": {
      "count": 2,
      "total_ms": 230,
      "max_ms": 141,
      "agent_audio_s": 31.2,
      "events": [
        [
          16.4,
          141
        ],
        [
          40.1,
          89
        ]
      ]
    },
    "quality": {
      "verdict": "Clean: no loss, no gaps.",
      "protocol": "UDP",
      "loss_pct": 0,
      "conceal_pct": 0,
      "jitter_ms": 4,
      "rtt_ms": 38
    },
    "cost": {
      "live": {
        "billed_s": 25.1,
        "reported": true,
        "voice_usd": 0.020917,
        "backend": {
          "in": 1840,
          "cached": 1024,
          "out": 96
        },
        "backend_usd": 0.000298
      },
      "llm": {
        "input_tokens": 1840,
        "cached_tokens": 1024,
        "output_tokens": 96,
        "usd": 0.000298
      },
      "tts": {
        "input_tokens": 0,
        "audio_tokens": 0,
        "audio_s": 25.1,
        "usd": 0.020917
      },
      "stt": {
        "audio_s": 0,
        "usd": 0
      }
    },
    "tools": [
      {
        "t": 14.2,
        "name": "check_order",
        "kind": "read",
        "ok": true,
        "ms": 412,
        "status": 200
      }
    ],
    "outcomes": [],
    "summary": {
      "text": "The caller asked for an appointment on Thursday afternoon; a message was taken for a callback.",
      "language": "en",
      "outcome": "message_taken",
      "sentiment": "neutral",
      "topics": [
        "appointment"
      ],
      "action_items": [
        "Call the caller back on Thursday morning"
      ]
    },
    "recording": {
      "available": true,
      "duration_s": 25.1,
      "size_bytes": 201344
    },
    "supervisors": [
      {
        "user": "ayse@example.com",
        "joined_at": 1793832420.4,
        "left_at": 1793832431,
        "modes": [
          "listen",
          "whisper"
        ]
      }
    ],
    "greeting": {
      "connect_s": 0.15,
      "session_started_s": 0.05,
      "reply_requested_s": 0.05,
      "first_audio_s": 2.4,
      "reply_done_s": 7.1
    },
    "transcript": [
      {
        "role": "assistant",
        "text": "Hello, thanks for calling the clinic. How can I help you today?",
        "t": 2.4,
        "interrupted": false
      },
      {
        "role": "user",
        "text": "I would like an appointment on Thursday afternoon.",
        "t": 9.8
      }
    ]
  },
  "quality": {
    "summary": {
      "verdict": "Clean: no loss, no gaps.",
      "protocol": "UDP",
      "rtt_ms": 38,
      "protocols_seen": [
        "UDP"
      ],
      "loss_pct": 0,
      "conceal_pct": 0,
      "jitter_ms": 4,
      "buffer_ms": 62
    },
    "timeline": [
      {
        "t": 2,
        "loss": 0,
        "conceal": 0,
        "jitter": 3,
        "buffer": 60,
        "proto": "UDP"
      },
      {
        "t": 4,
        "loss": 0,
        "conceal": 0,
        "jitter": 4,
        "buffer": 62,
        "proto": "UDP"
      }
    ]
  }
}

Errors

  • 404 Call not found.

Download the recording

GET /v1/calls/{room}/recording

The call's audio as OGG/Opus. Supports Range requests. 404 if the call was not recorded or the recording has expired.

NameInTypeDescription
room
required
path string Room name from the call list or from POST /v1/web-calls.

Request

curl
curl "https://hubvocal.com/v1/calls/demo-8585522d/recording" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -o call.ogg

Response · 200

audio/ogg
HTTP/1.1 200 OK
Content-Type: audio/ogg
Accept-Ranges: bytes
Content-Length: 201344

<OGG/Opus>

Errors

  • 404 The call was not recorded or the recording has expired.

Delete a call

DELETE /v1/calls/{room}

Deletes the call's recording, transcript and summary immediately, for example for a KVKK or GDPR request. The call row and its cost remain.

NameInTypeDescription
room
required
path string Room name from the call list or from POST /v1/web-calls.

Request

curl
curl -X DELETE "https://hubvocal.com/v1/calls/demo-8585522d" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
{
  "deleted": "demo-8585522d"
}

Errors

  • 404 Call not found.

Export calls (CSV)

GET /v1/calls/export

Calls in a date range as CSV, one row per call, with structured fields as extra columns.

NameInTypeDescription
format
required
query string Only csv.
from query date YYYY-MM-DD, inclusive.
to query date YYYY-MM-DD, inclusive.
agent_id query string Only calls of this agent.

Request

curl
curl "https://hubvocal.com/v1/calls/export?format=csv&from=2026-11-01&to=2026-11-05" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -o calls.csv

Response · 200

text/csv
room,started_at,agent,channel,duration_s,total_usd,outcome,appointment_date,callback
demo-8585522d,2026-11-05T09:46:49Z,front-desk,web,25.1,0.021215,message_taken,2026-11-12,true

Phone numbers

List phone numbers

GET /v1/numbers

Your organization's phone numbers and the agent that answers each one (profile_id), plus the readiness of each telephony provider. Numbers are added by the platform team; you assign agents.

Request

curl
curl "https://hubvocal.com/v1/numbers" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
{
  "numbers": [
    {
      "number": "+908501234567",
      "provider": "didww",
      "org_id": "6f1c2a40-3b7e-4d0c-9a51-2f8e7c1d9b03",
      "org_name": "Example Clinic",
      "profile_id": "front-desk",
      "label": "Main line"
    }
  ],
  "sip": {
    "enabled": true,
    "room_prefix": "tel-",
    "providers": {
      "didww": {
        "inbound_ips": "46.19.209.14,46.19.210.14",
        "inbound_auth": false,
        "outbound_host": "trunk.example.com",
        "outbound_auth": true,
        "outbound_ready": true,
        "inbound_ready": true
      }
    }
  }
}

Assign an agent to a number

PUT /v1/numbers/{number}

Chooses the agent that answers incoming calls on this number. Send agent_id: null to unassign. Encode the + of the number as %2B in the path (00 prefix also works).

NameInTypeDescription
number
required
path string The number in E.164, URL-encoded.
agent_id body string | null Agent that answers incoming calls.
label body string A note for your team, up to 60 characters.

Request

curl
curl -X PUT "https://hubvocal.com/v1/numbers/%2B908501234567" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "agent_id": "front-desk",
  "label": "Main line"
}'

Response · 200

JSON
{
  "ok": true
}

Errors

  • 400 The agent does not exist.
  • 404 The number does not belong to your organization.

Usage

Usage

GET /v1/usage

Calls, minutes and provider cost for your organization, by provider and by day (Europe/Istanbul). Defaults to the current month. kind is llm, tts, stt or voice (speech-to-speech engines).

NameInTypeDescription
from query date YYYY-MM-DD, inclusive.
to query date YYYY-MM-DD, inclusive.

Request

curl
curl "https://hubvocal.com/v1/usage?from=2026-11-01&to=2026-11-05" \
  -H "Authorization: Bearer $VT_API_KEY"

Response · 200

JSON
{
  "from": "2026-11-01",
  "to": "2026-11-05",
  "totals": {
    "calls": 42,
    "minutes": 81.2,
    "usd": 3.94
  },
  "by_provider": [
    {
      "provider": "openai",
      "model": "gpt-live-1",
      "kind": "voice",
      "calls": 30,
      "input_tokens": 0,
      "cached_tokens": 0,
      "output_tokens": 0,
      "audio_s": 2900.1,
      "usd": 2.41
    }
  ],
  "by_day": [
    {
      "date": "2026-11-05",
      "usd": 1.2,
      "minutes": 20.5,
      "calls": 9
    }
  ],
  "by_org": [
    {
      "org_id": "6f1c2a40-3b7e-4d0c-9a51-2f8e7c1d9b03",
      "org_name": "Example Clinic",
      "usd": 3.94,
      "minutes": 81.2,
      "calls": 42
    }
  ]
}

Specification

OpenAPI specification

GET /v1/openapi.json No key required

The OpenAPI 3.1 document for this API. No key required. Import it into Postman, Insomnia or a client generator.

Request

curl
curl "https://hubvocal.com/v1/openapi.json"

Response · 200

JSON
{
  "openapi": "3.1.0",
  "info": {
    "title": "HubVocal API",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://hubvocal.com/v1"
    }
  ],
  "paths": {}
}