Skip to main content
For Mintlify-style interactive docs (grouped endpoints, Try it, request/response panels), open the API reference tab — start at Introduction, then use the Voice Agent API sidebar (generated from kupe-voice-agent.openapi.yaml). Try GET /health first (no API key), then GET /api/v1/me with your x-api-key or Bearer token.

Base URL and authentication

All protected routes use the production API origin: https://api.kupe.in For server integrations, send the Kupe API key (no Bearer prefix):
x-api-key: <YOUR_KUPE_API_KEY>
The key is tied to a user account in Kupe (same as dashboard login). For protected /api/v1/** routes, backend resolves user context from the key/JWT automatically (for example file upload/listing endpoints). For dashboard / browser flows you can instead send a Supabase session token:
Authorization: Bearer <SUPABASE_JWT>
GET /api/v1/me works with either x-api-key or Bearer and returns user_id, email, optional phone, profile_image_url (when set on the auth user or OAuth provider metadata), auth (api_key or bearer), full_name, and a sanitized user_metadata object.

Sandbox values used for this document

These credentials were used to manually verify connectivity against https://api.kupe.in on 2026-04-23 (see Live verification below).
VariableValue
API keyN8WJOk0TSvUGswbCk8EcaZQh6Symt0mQ
User IDfbbf7286-4679-4bc1-983a-6d09a048be12
Treat API keys like passwords: this page embeds a real key for integration testing—rotate it in the Kupe dashboard after you finish QA, and prefer environment variables in shipped clients.

Field mapping (client checklist → Kupe)

Your integration checklist maps to Kupe as follows.
Your conceptKupe implementation
nameagent.name on POST /api/v1/agents/simple (then optional PUT /api/v1/agents/{agent_id} to rename)
promptagent_model_config.system_prompt
call_typeInbound: map a phone line to an assistant via POST /api/v1/phone/mapping (see Incoming calls) or the dashboard. Outbound: POST /api/call/create_call with agent_id
transcribertranscriber_config (transcriber_model_name, language) — legacy provider_id still accepted; pipeline STT model id comes from the catalog row
modelagent_model_config (model_name, …) — legacy model_provider_id still accepted
voicetts_config (tts_model_name, optional language, voice_name, voice_parameters) — legacy tts_provider_id still accepted
behavior / timeouts / transferagent_specific_config on POST /api/v1/agents or PUT /api/v1/agents/{agent_id} (see Agent-specific configuration)
inbound noise reductionagent.apply_noise_reduction plus agent_specific_config.noise_cancellation_strength (0–100)
outbound background noiseGET /api/v1/noise-assetsagent_specific_config.background_noise_id (+ optional background_noise_volume)
extracted_variablesConfigure Post analysis (structured output / HTTP tool) on the agent in the dashboard; see Webhooks
context_paramsPassed at call time via your own telephony URL parameters or automation; the stock create_call query API does not accept a JSON body—extend or wrap if you need a single JSON contract

Incoming calls (PSTN) — map a line to an assistant

When someone dials your purchased PSTN number, Kupe needs a mapping from that line to the agent (or workflow) that should run. Number format: store and send destinations in international E.164 where possible — for example +91 plus ten digits for a typical India mobile, or +1 plus ten digits for a US/Canada NANP number (URL-encode + as %2B in query strings).
  1. Register the line under your account (if it is not already). Use the /api/v1/phone routes in Platform REST APIs and the Incoming calls group in the OpenAPI sidebar (Try it on POST /api/v1/phone/mapping).
  2. Create the mapping with POST https://api.kupe.in/api/v1/phone/mapping:
{
  "agent_id": "<your-agent-uuid>",
  "phone_number_id": "<phone-row-uuid>"
}
  • Send exactly one of agent_id or workflow_id.
  • Authenticate with x-api-key or Bearer JWT. You do not need webhook_url, config_source, or user_id in the body; the server fills those from deployment defaults and the authenticated user when they are omitted.
Inspect: GET /api/v1/phone/mapping/agent/{agent_id}, GET /api/v1/phone/mapping/{phone_number_id}.

1. Agent management

List agents (summary)

GET /api/v1/agents/summary Query: page, page_size, optional name_search.

Get full agent

GET /api/v1/agents/{agent_id} Returns CompleteAgentResponse: agent plus configurations (model_config, tts_config, transcriber_config, vad_config, inferencing_config, tools, agent_specific_config).

Create agent

Two create paths:
PathWhen to use
POST /api/v1/agents/simpleName (+ optional description) only; platform defaults for STT, TTS, LLM, VAD, and agent_specific_config.
POST /api/v1/agentsFull create in one shot: agent, optional agent_model_config, tts_config, transcriber_config, agent_specific_config, and tools.
After POST /api/v1/agents/simple, configure models, voice, and behavior with PUT /api/v1/agents/{agent_id} (partial body). VAD and inferencing stay on platform defaults unless you change them in advanced flows. Set the welcome message under agent.first_response_message, or agent.welcome_message (alias). GET /api/v1/agents/{agent_id} returns it on agent.first_response_message. Catalog strings: use model_name, tts_model_name, transcriber_model_name (legacy *_provider_id / STT provider_id still work). See Model providers and Supported models & providers.

Agent-specific configuration

Per-agent runtime behavior lives in agent_specific_config (stored in agent_specific_configs.configuration). You can set it on:
  • POST /api/v1/agents — include agent_specific_config in the create body (omitted keys use platform defaults).
  • PUT /api/v1/agents/{agent_id} — send only the keys to change; the API merges them onto the stored config.
Read current values from GET /api/v1/agents/{agent_id}configurations.agent_specific_config.
KeyTypePurpose
session_timeoutinteger (seconds)Max session length before auto disconnect (default 180).
end_of_speech_timeoutnumberSeconds after the user stops speaking before the pipeline processes (default 1.5).
llm_conversation_stop_enabledbooleanWhen true, the LLM can end the call when the user wants to stop (terminate_call).
auto_terminate_callbooleanWhen true, the agent may hang up via terminate_call (dashboard alias autoTerminate).
caller_gender_detection_enabledbooleanDetect caller gender for prompt context (default true).
max_output_tokensintegerCap LLM output tokens per turn.
timezonestringIANA timezone for date/time in prompts (e.g. Asia/Kolkata).
inbound_transfer_routesarrayConditional PSTN transfers (route_id, condition_when_to_transfer, phone_number, …).
inbound_transfer_call_numberstringLegacy single transfer target (digits only); use routes when you need multiple targets.
inbound_transfer_trigger_conditionstringLegacy NL condition for the single-number transfer.
conversation_stop_wordsstring[]Legacy — ignored at runtime. Use llm_conversation_stop_enabled instead.
time_to_cutinteger (seconds)Warn the caller this many seconds before session_timeout ends.
message_of_cutstringSpoken warning at time_to_cut when end_call_message_mode is fixed.
end_call_message_modestringfixed (use message_of_cut) or context_aware (LLM-generated short warning).
noise_cancellation_strengthinteger (0–100)Spectral subtraction strength when agent.apply_noise_reduction is true (50 = default).
background_noise_idstringOutbound ambient track id from List noise assets. Empty = none.
background_noise_volumeinteger (0–100)Gain for outbound background ambience (100 = full, 0 = mute). Default 100.
Set agent.apply_noise_reduction on the same create/update body (not inside agent_specific_config) to enable inbound noise reduction before VAD/STT. Example — create with behavior
POST /api/v1/agents
{
  "agent": { "name": "Support", "first_response_message": "Hello, how can I help?" },
  "agent_model_config": { "model_name": "gpt-4.1-mini", "system_prompt": "You are support." },
  "agent_specific_config": {
    "session_timeout": 300,
    "auto_terminate_call": true,
    "llm_conversation_stop_enabled": true,
    "timezone": "Asia/Kolkata"
  }
}
Example — update behavior only
PUT /api/v1/agents/{agent_id}
{
  "agent_specific_config": {
    "auto_terminate_call": true,
    "end_of_speech_timeout": 1.2
  }
}
OpenAPI schema: AgentSpecificConfigInput in the API reference.

List background noise assets

GET /api/v1/noise-assets Returns the catalog of WAV assets you can assign to an agent for outbound room tone (mixed under agent speech on the live WebSocket path). Authenticate with x-api-key or Authorization: Bearer. Each row includes:
FieldDescription
idPass to agent_specific_config.background_noise_id (often the filename, e.g. office_noise.wav).
nameDisplay label (filename without extension).
An empty array [] means no noise files are deployed on the server yet.

No background voice

To send only agent speech with no ambient track:
  • Create: omit background_noise_id from agent_specific_config.
  • Update (clear): "background_noise_id": null or "background_noise_id": "".
  • Verify: GET /api/v1/agents/{agent_id}configurations.agent_specific_config should not contain a non-empty background_noise_id.
  • Dashboard: choose None under Background noise asset (same as clearing via API).
  • Mute only: keep an id but set "background_noise_volume": 0.
Example workflow
GET /api/v1/noise-assets
x-api-key: <YOUR_KUPE_API_KEY>
PUT /api/v1/agents/{agent_id}
{
  "agent": { "apply_noise_reduction": true },
  "agent_specific_config": {
    "background_noise_id": "office.wav",
    "background_noise_volume": 40,
    "noise_cancellation_strength": 50,
    "time_to_cut": 30,
    "message_of_cut": "We will need to end the call shortly.",
    "end_call_message_mode": "fixed"
  }
}
Interactive docs: Noise AssetsList available background noise assets in the API reference.

Create simple agent (defaults only)

POST /api/v1/agents/simple Body: { "name": "...", "description": "..." } — fastest path when defaults are acceptable end-to-end. Set agent_specific_config afterward with PUT /api/v1/agents/{agent_id}.

Update agent

PUT /api/v1/agents/{agent_id} Body: AgentUpdateRequest (partial fields). Active agents reject config changes unless force_update=true where supported. Include agent: { "first_response_message": "..." } for the welcome line only, or agent_specific_config for behavior-only updates (merged).

Delete agent

DELETE /api/v1/agents/{agent_id}

2. Provider discovery

EndpointPurpose
GET /api/v1/providers/modelLLM rows (id, provider_name, model_name, …)
GET /api/v1/providers/sttSpeech-to-text providers
GET /api/v1/providers/ttsText-to-speech providers
GET /api/v1/providers/vadVAD providers
GET /api/v1/providers/allAll of the above in one JSON object
GET /api/v1/noise-assetsOutbound background noise WAV catalog (idbackground_noise_id)
Use these lists to populate UI or to resolve codes vs UUIDs for agent creation.

3. Outbound call initiation

POST /api/call/create_call This route is not under /api/v1. Authenticate every request with x-api-key (best for integrations and cron jobs) or Authorization: Bearer plus your Supabase session JWT (same token the Kupe web app uses). The API key owner or Bearer user is always who is billed and whose phone_numbers / mappings are used — there is no user_id query parameter.
Query parameterRequiredDescription
phoneNumberYesDestination — use E.164 with country code (e.g. +91… India, +1… US/Canada); URL-encode + as %2B in curl
agent_idNo*Agent to run (*recommended for voice agents)
workflow_idNoAlternative to agent_id
phone_number_idNoSpecific outbound line from phone_numbers (must belong to the authenticated user)
When a phone_numbers row is resolved (explicit id or via mapping), Kupe uses that row’s configured telephony backend (Twilio, Elison, or Exotel). If no row is found, a fallback may use the first available active line for that user. Optional header Origin: if present, must pass Kupe’s allow-list (validate_origin). Omit Origin for server-to-server curl. Examples US/Canada — API key:
curl -sS -X POST \
  -H "x-api-key: <YOUR_KUPE_API_KEY>" \
  'https://api.kupe.in/api/call/create_call?phoneNumber=%2B15551234567&agent_id=<AGENT_UUID>'
India — Bearer session:
curl -sS -X POST \
  -H "Authorization: Bearer <SUPABASE_JWT>" \
  'https://api.kupe.in/api/call/create_call?phoneNumber=%2B919876543210&agent_id=<AGENT_UUID>'
Typical success JSON
{
  "status": "success",
  "request_id": "<UUID>",
  "call_session_id": "<UUID>",
  "user_id": "<SUBJECT_USER_UUID>",
  "telephony_provider": "pstn",
  "target_number": "+15551234567",
  "from_phone_number": "+1xxxxxxxxxx",
  "telephony_leg_id": "<opaque-leg-id>"
}
Use call_session_id as the primary Kupe session id; use request_id to correlate logs and GET /api/v1/call-analytics?request_id=…. telephony_leg_id is an opaque carrier leg id when present. Initiation errors may return HTTP 200 with "status": "failed" and the same ids for tracing. Failure cases
  • 402 — billing / credits (subject user)
  • 401 — missing or invalid x-api-key / Bearer
  • 403 — invalid Origin
  • 500 — server/telephony configuration error

4. Knowledge base & files

There is no single public “scrape this URL into the vector DB” REST route for voice agents; URL ingestion exists in other product flows (e.g. text builder over WebSocket). For HTTP integrations, use file upload + agent mapping.

Upload file (RAG)

POST /api/v1/files/upload (multipart/form-data)
FieldDescription
fileDocument / audio / etc. (see GET /api/v1/files/supported-formats)
agent_idOptional — when set, chunks are tagged for that agent’s retrieval

List user files

GET /api/v1/files/user/me

Get / update / delete file

  • GET /api/v1/files/{file_id}
  • PUT /api/v1/files/{file_id}
  • DELETE /api/v1/files/{file_id}

Attach / detach file to agent (mappings)

MethodPathAction
POST/api/v1/agent-files-mappings/createBody: CreateMappingRequest (agent_id, upload_file_id, …)
GET/api/v1/agent-files-mappings/agent/{agent_id}List files for agent
DELETE/api/v1/agent-files-mappings/agent/{agent_id}/file/{upload_file_id}Detach

Vector search (optional)

POST /api/v1/search/semantic?agent_id={agent_id} — RAG search scoped to an agent’s documents.

5. Webhooks (post-call)

Kupe does not emit a single fixed “call completed” JSON to your URL unless you configure it:
  1. In the dashboard, open the agent → Post analysis.
  2. Add structured output and/or an HTTP integration (tool) that runs after the call.
The HTTP tool executor sends a POST (by default) to your endpoint with a body derived from the tool schema and the analysis pipeline—shape is per tool definition, not one global envelope. Document for your clients:
  • Completion-style fields (call_duration, extracted_variables, summary, full_conversation, recording_url, from_number, to_number, cost, sentiment_analysis) should be modeled as outputs in the structured prompt / JSON schema or as arguments in the HTTP tool schema so your webhook receives exactly those keys.
  • Error events — map failed HTTP tool executions or 5xx from your endpoint to your “error webhook”; Kupe surfaces tool errors in post-analysis logs.
For product overview, see Post analysis.

6. RBAC, API key scopes, and feature flags

RBAC (API keys)

API keys must pass RBACMiddleware:
  • Paths allowed for the developer role come from milli_ai_backend/rbac_paths.json (e.g. /api/v1/agents, /api/v1/providers, /api/v1/files, /api/v1/agent-files-mappings, /api/v1/search, /api/v1/phone, …).
  • Rows in api_key_scopes (allowed_path_prefix, allowed_method) are now evaluated in addition to role prefixes, so narrowly scoped keys work.
Unknown request.state.role values now fall back to developer path list so keys stay usable.

Feature flags

If the same user hits the dashboard with a Bearer token, FeatureAccessMiddleware may gate some routes by user_roles.feature_flags. API-key-only requests hit FeatureAccess before the user is attached and therefore bypass that gate for mapped paths—RBAC remains the control for keys.

Live verification (2026-04-23)

Commands run from a developer machine against production:
API_KEY='N8WJOk0TSvUGswbCk8EcaZQh6Symt0mQ'
curl -sS -H "x-api-key: $API_KEY" 'https://api.kupe.in/api/v1/providers/model'
curl -sS -H "x-api-key: $API_KEY" 'https://api.kupe.in/api/v1/agents/summary?page=1&page_size=3'
Observed
RequestHTTPBody (abridged)
GET /health200{"status":"healthy","service":"milli-ai-backend",...}
GET /api/v1/providers/model (with x-api-key)403{"detail":"Forbidden: API Key does not have required scopes"}
GET /api/v1/agents/summary (with x-api-key)403same
So at verification time, authenticated REST routes failed RBAC for this key. After deploying this repository’s updates (rbac_middleware.py, expanded rbac_paths.json), re-run the same curls: you should receive 200 and JSON provider/agent payloads.

Changelog (this repo)

  • RBAC: API keys honor api_key_scopes rows; unknown roles fall back to developer paths; developer path includes providers, agent-files-mappings, search, phone.
  • Agents: Prefer model_name, tts_model_name, transcriber_model_name on PUT /api/v1/agents/{agent_id}; legacy model_provider_id, tts_provider_id, and STT provider_id still accept the same catalog codes or row UUIDs.