# Agents API

> Create, read, update and delete voice agents over REST — full field reference including prompt, languages, LLM/STT/TTS providers, variables, transfer and webhook configuration.

Source: https://edesy.in/docs/voice-agent/api/agents

An agent is the voice bot: its prompt, language, and its LLM / STT / TTS / telephony
configuration.

| Method | Path | Scope |
|--------|------|-------|
| `GET` | `/api/v1/agents` | `agents:read` |
| `POST` | `/api/v1/agents` | `agents:write` |
| `GET` | `/api/v1/agents/{id}` | `agents:read` |
| `PATCH` | `/api/v1/agents/{id}` | `agents:write` |
| `DELETE` | `/api/v1/agents/{id}` | `agents:write` |

Authentication, scopes and the response envelope are covered in the
[API Overview](https://edesy.in/docs/voice-agent/api).

## Create an agent

`POST /api/v1/agents` → **201 Created**

Only `name` and `language` are required; everything else has a platform default.

<!-- tabs -->

```bash
curl -X POST https://voice-agent.edesy.in/api/v1/agents \
  -H "Authorization: Bearer $EDESY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Order Status Bot",
    "language": "hindi_english",
    "prompt": "You are a support agent for Acme. Ask for the order number, look it up, and read back the status. Be concise and polite.",
    "greetingMessage": "Namaste! Acme support se baat kar rahe hain. Aapka order number bataiye.",
    "llmProvider": "gemini-live-2.5",
    "geminiliveVoice": "Aoede",
    "callProvider": "twilio",
    "active": true
  }'
```

```python
import os
import requests

response = requests.post(
    "https://voice-agent.edesy.in/api/v1/agents",
    headers={"Authorization": f"Bearer {os.environ['EDESY_API_KEY']}"},
    json={
        "name": "Order Status Bot",
        "language": "hindi_english",
        "prompt": "You are a support agent for Acme. Ask for the order number, look it up, and read back the status. Be concise and polite.",
        "greetingMessage": "Namaste! Acme support se baat kar rahe hain. Aapka order number bataiye.",
        "llmProvider": "gemini-live-2.5",
        "geminiliveVoice": "Aoede",
        "callProvider": "twilio",
        "active": True,
    },
    timeout=30,
)
response.raise_for_status()

agent = response.json()["data"]
print(agent["id"])
```

```javascript
const response = await fetch("https://voice-agent.edesy.in/api/v1/agents", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.EDESY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Order Status Bot",
    language: "hindi_english",
    prompt:
      "You are a support agent for Acme. Ask for the order number, look it up, and read back the status. Be concise and polite.",
    greetingMessage:
      "Namaste! Acme support se baat kar rahe hain. Aapka order number bataiye.",
    llmProvider: "gemini-live-2.5",
    geminiliveVoice: "Aoede",
    callProvider: "twilio",
    active: true,
  }),
});

if (!response.ok) {
  throw new Error(`Edesy API ${response.status}: ${await response.text()}`);
}

const { data: agent } = await response.json();
console.log(agent.id);
```

```php
<?php
$ch = curl_init('https://voice-agent.edesy.in/api/v1/agents');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . getenv('EDESY_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'name'            => 'Order Status Bot',
        'language'        => 'hindi_english',
        'prompt'          => 'You are a support agent for Acme. Ask for the order number, look it up, and read back the status. Be concise and polite.',
        'greetingMessage' => 'Namaste! Acme support se baat kar rahe hain. Aapka order number bataiye.',
        'llmProvider'     => 'gemini-live-2.5',
        'geminiliveVoice' => 'Aoede',
        'callProvider'    => 'twilio',
        'active'          => true,
    ]),
]);

$response = curl_exec($ch);
$status   = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status !== 201) {
    throw new RuntimeException("Edesy API {$status}: {$response}");
}

$agent = json_decode($response, true)['data'];
echo $agent['id'];
```

<!-- /tabs -->

<!-- response 201 -->
```json
{
  "success": true,
  "data": {
    "id": 1042,
    "name": "Order Status Bot",
    "language": "hindi_english",
    "active": true,
    "greetingMessage": "Namaste! Acme support se baat kar rahe hain. Aapka order number bataiye.",
    "llmProvider": "gemini-live-2.5",
    "llmModel": "gemini-2.5-flash",
    "sttProvider": null,
    "ttsProvider": null,
    "ttsVoice": null,
    "agentType": "AI_POWERED",
    "createdAt": "2026-08-10T09:12:44.318Z",
    "updatedAt": "2026-08-10T09:12:44.318Z"
  }
}
```

Defaults applied when omitted: `active` = `true`, `llmProvider` = `gemini`,
`llmModel` = `gemini-2.5-flash`.

Supplying a `prompt` also seeds **version 1** of the agent's prompt history, so the
version timeline in the dashboard matches the live prompt from the start.

**Errors:** `400 VALIDATION_ERROR`, `409 NO_ATTRIBUTABLE_USER` (the workspace has no
members, so the write cannot be attributed to a user).

## List agents

`GET /api/v1/agents`

| Query | Type | Description |
|-------|------|-------------|
| `search` | string | Case-insensitive substring match on name |
| `language` | string | Exact language match, e.g. `hindi` |
| `active` | `true` \| `false` | Filter by active state. **Omit for no filter** |
| `limit` | integer | Default 50, max 100 |
| `offset` | integer | Default 0 |

<!-- response 200 -->
```json
{
  "success": true,
  "data": { "agents": [], "total": 37, "limit": 50, "offset": 0 }
}
```

List results are summaries. Use `GET /api/v1/agents/{id}` for the full configuration
(prompt, `llmConfig`, `sttConfig`, `ttsConfig`, `variables`, `transferConfig`, webhook
fields, and more).

## Get an agent

`GET /api/v1/agents/{id}` → **200 OK**, returning the full configurable surface.

A non-numeric id returns `400 INVALID_AGENT_ID`; an unknown id returns `404 NOT_FOUND`.

## Update an agent

`PATCH /api/v1/agents/{id}` → **200 OK**

Partial update — **only the fields you send are written**. Omitted fields are left
untouched; sending `null` clears a nullable field.

<!-- tabs -->

```bash
curl -X PATCH https://voice-agent.edesy.in/api/v1/agents/1042 \
  -H "Authorization: Bearer $EDESY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "You are a support agent for Acme. Confirm the order number digit by digit before looking it up.",
    "llmTemperature": 0.3,
    "webhookUrl": "https://example.com/hooks/edesy",
    "webhookEnabled": true
  }'
```

```python
import os
import requests

response = requests.patch(
    "https://voice-agent.edesy.in/api/v1/agents/1042",
    headers={"Authorization": f"Bearer {os.environ['EDESY_API_KEY']}"},
    json={
        "prompt": "You are a support agent for Acme. Confirm the order number digit by digit before looking it up.",
        "llmTemperature": 0.3,
        "webhookUrl": "https://example.com/hooks/edesy",
        "webhookEnabled": True,
    },
    timeout=30,
)
response.raise_for_status()
```

```javascript
const response = await fetch(
  "https://voice-agent.edesy.in/api/v1/agents/1042",
  {
    method: "PATCH",
    headers: {
      Authorization: `Bearer ${process.env.EDESY_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      prompt:
        "You are a support agent for Acme. Confirm the order number digit by digit before looking it up.",
      llmTemperature: 0.3,
      webhookUrl: "https://example.com/hooks/edesy",
      webhookEnabled: true,
    }),
  }
);

if (!response.ok) {
  throw new Error(`Edesy API ${response.status}: ${await response.text()}`);
}
```

```php
<?php
$ch = curl_init('https://voice-agent.edesy.in/api/v1/agents/1042');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . getenv('EDESY_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'prompt'         => 'You are a support agent for Acme. Confirm the order number digit by digit before looking it up.',
        'llmTemperature' => 0.3,
        'webhookUrl'     => 'https://example.com/hooks/edesy',
        'webhookEnabled' => true,
    ]),
]);

$response = curl_exec($ch);
$status   = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status !== 200) {
    throw new RuntimeException("Edesy API {$status}: {$response}");
}
```

<!-- /tabs -->

Returns the full agent object. Two things happen automatically:

1. **Prompt versioning.** Changing `prompt` creates a new prompt version and points the
   agent at it, so the version history never drifts from the live prompt. Every version
   is visible and restorable from the dashboard.
2. **Live propagation.** Changing the prompt, greeting, voice or provider config
   invalidates the voice backends' caches. A `200` means the change applies to the
   **next call** — not up to 24 hours later. A rollback-able configuration snapshot is
   also captured.

**Errors:** `400 VALIDATION_ERROR`, `404 NOT_FOUND`, `409 NO_ATTRIBUTABLE_USER`.

## Delete an agent

`DELETE /api/v1/agents/{id}` → `{ "success": true }`

**Errors:** `404 NOT_FOUND`.

## Field reference

All fields below are accepted by both `POST /api/v1/agents` and
`PATCH /api/v1/agents/{id}`. On create, `name` and `language` are required.

### Identity and behaviour

| Field | Type | Notes |
|-------|------|-------|
| `name` | string (1–100) | Required on create |
| `language` | enum | Required on create. See [Languages](#languages) |
| `additionalLanguages` | enum[] (max 8) | Extra languages for a multilingual agent; `language` stays primary |
| `prompt` | string \| null | The system prompt. Changing it creates a new prompt version |
| `greetingMessage` | string (≤500) \| null | First thing the agent says |
| `endCallInstructions` | string (≤2000) \| null | How the agent should wrap up and hang up |
| `summaryPrompt` | string (≤5000) \| null | Custom prompt for the post-call summary. Supports a `{transcript}` placeholder; empty → platform default |
| `active` | boolean | Inactive agents will not answer or place calls |
| `agentType` | `AI_POWERED` \| `FLOW_BASED` \| `AI_FLOW` | Which engine answers. Default `AI_POWERED` |
| `variables` | object[] | Declared prompt variables — see [Variables](#variables) |

### LLM

| Field | Type | Notes |
|-------|------|-------|
| `llmProvider` | string | `openai`, `anthropic`, `groq`, `google`, `gemini-2.5`, `gemini-live`, `gemini-live-2.5`, `gemini-live-3.1`, `openai-realtime`, `openai-realtime-mini` |
| `llmModel` | string | Model id for the provider — see the [catalog](https://edesy.in/docs/voice-agent/api/provider-catalog) |
| `llmTemperature` | number 0–1 | |
| `llmMaxTokens` | number 1–100000 | |
| `llmConfig` | object | Provider-specific tuning — see [llmConfig](#llmconfig) |
| `geminiliveVoice` | string | Voice for native-audio (Gemini Live) providers |

> **Provider and model ids are not validated on write.** These fields are plain strings
> — a typo returns `200` and only surfaces when a call is placed, where the runtime may
> silently fall back to a default rather than failing. Copy ids exactly from the
> [Provider & Voice Catalog](https://edesy.in/docs/voice-agent/api/provider-catalog).

### Speech

| Field | Type | Notes |
|-------|------|-------|
| `sttProvider` | string | `deepgram`, `openai_whisper`, `azure`, `google_stt`, `assemblyai`, `sarvam_stt`, `soniox_stt`, `elevenlabs_stt`, `speechmatics` |
| `sttModel` | string | See the [catalog](https://edesy.in/docs/voice-agent/api/provider-catalog) |
| `sttConfig` | object | Provider-specific STT options |
| `ttsProvider` | string | `google`, `openai`, `elevenlabs`, `azure_tts`, `cartesia`, `sarvam`, `soniox`, `heypixa`, `deepgram_tts`, `playht`, `gemini_genai_tts`, `gemini_tts` |
| `ttsVoice` | string | Voice id — see the [catalog](https://edesy.in/docs/voice-agent/api/provider-catalog) (541 voices) |
| `ttsConfig` | object | Provider-specific TTS options |

> **The speech-to-text and text-to-speech id lists are not interchangeable.** Several
> providers carry a modality suffix on one side only: speech-to-text uses
> `elevenlabs_stt`, `google_stt`, `sarvam_stt`, `openai_whisper`; text-to-speech uses
> `elevenlabs`, `google`, `sarvam`, `openai`, `azure_tts`, `deepgram_tts`. Using a TTS
> id in `sttProvider` is accepted on write and fails at call time.

STT and TTS are ignored for native audio-to-audio providers (`gemini-live-*`,
`openai-realtime-*`), which handle speech end to end.

### Telephony and integration

| Field | Type | Notes |
|-------|------|-------|
| `callProvider` | `twilio` \| `exotel` \| `plivo` \| `vobiz` \| `alohaa` \| `telnyx` \| `edesy-ivr` \| `edesy-sip` \| `edesy` | Carrier used for this agent's calls |
| `webhookUrl` | URL (≤2000) \| null | Post-call webhook target |
| `webhookHeaders` | object | Headers sent with the webhook |
| `webhookEnabled` | boolean | |
| `transferConfig` | object \| null | Warm/cold transfer and DTMF routing |

```json
{
  "transferConfig": {
    "enabled": true,
    "announceMessage": "Please hold, connecting you to our team.",
    "destinations": [
      { "id": "sales", "name": "Sales", "phoneNumber": "+919876543210", "description": "Sales desk" }
    ],
    "dtmfTransferEnabled": true,
    "dtmfMappings": [
      { "digit": "1", "destinationId": "sales", "announceMessage": "Connecting to Sales." }
    ]
  }
}
```

Destination phone numbers are normalised to E.164 on save.

> `workspaceId` is **not** part of this API. Your key is workspace-scoped and the
> workspace always comes from the key. If you send a `workspaceId` in the body it is
> silently ignored — it will **not** move the agent into another workspace, and it is
> not an error.

## Languages

`language` and `additionalLanguages` accept 112 lowercase language ids across three
groups — single languages (`hindi`, `tamil`, `assamese`, …), bilingual pairs
(`hindi_english`, `tamil_english`, …), and multilingual combinations. `multilingual`
auto-detects the caller's language at runtime on native-audio providers.

Unlike provider ids, **languages are validated** — an unknown value returns
`400 VALIDATION_ERROR` with the message `Invalid language selection`. The complete list
is in the [Provider & Voice Catalog](https://edesy.in/docs/voice-agent/api/provider-catalog).

## Variables

Declare the placeholders your prompt uses so they can be supplied per call.

```json
{
  "variables": [
    { "name": "customer_name", "type": "string", "label": "Customer name", "required": true },
    { "name": "order_id", "type": "string", "description": "Order reference", "required": true },
    { "name": "due_amount", "type": "number", "defaultValue": 0, "required": false }
  ]
}
```

| Field | Type | Notes |
|-------|------|-------|
| `name` | string (≤50) | Required. Referenced in the prompt as `{{name}}` |
| `type` | `string` \| `number` \| `boolean` \| `date` | Required |
| `description` | string (≤200) | |
| `defaultValue` | any | Used when the call does not supply the variable |
| `required` | boolean | Default `false` |
| `label` | string (≤100) | Display label |
| `fieldType` | `short_text` \| `paragraph` \| `select` \| `number` \| `checkbox` \| `date` | Input widget |
| `options` | string[] | For `select` |
| `maxLength` | number | For text types |
| `hidden` | boolean | Hide from input forms |

Values are supplied at call time via `variables` on
[`POST /api/v1/calls`](https://edesy.in/docs/voice-agent/api/calls). Both `{{key}}` and `{key}`
placeholder forms are substituted, in the prompt and the greeting. Prefer multi-word
variable names — a single-word one can capture an incidental brace pair in the prompt.

## llmConfig

A free-form JSON object for provider tuning. It is **replaced wholesale** on write, so
always read → modify → write back the whole object. Unknown keys are stored and
ignored, which means a typo is silent — copy the keys below exactly.

### Turn-taking

How long the agent waits for silence before deciding the caller has stopped speaking.

| `vadProfile` | Silence | Use for |
|--------------|---------|---------|
| `low_latency` *(default)* | 100 ms | Sales, fast-paced conversation |
| `balanced` | 200 ms | Most agents; avoids cutting people off mid-sentence |
| `conservative` | 350 ms | Hindi, Gujarati, Marathi, Assamese, Malayalam; elderly callers |

```json
{ "llmConfig": { "vadProfile": "balanced" } }
```

### Noise filtering

Two independent layers for noisy environments. Applies to the STT → LLM → TTS pipeline,
not to native-audio providers.

```json
{
  "llmConfig": {
    "noiseFiltering": {
      "enabled": true,
      "sileroVad": { "confidence": 0.8, "minVolume": 0.7, "startSecs": 0.25, "stopSecs": 0.8 },
      "turnStrategy": { "minWords": 2, "useInterim": true }
    }
  }
}
```

| Key | Default | Range | Meaning |
|-----|---------|-------|---------|
| `sileroVad.confidence` | 0.7 | 0–1 | Speech-detection threshold; higher is stricter |
| `sileroVad.minVolume` | 0.6 | 0–1 | Ignore audio quieter than this |
| `sileroVad.startSecs` | 0.2 | 0.1–1.0 | Speech needed before detection starts |
| `sileroVad.stopSecs` | 0.8 | 0.1–2.0 | Silence needed before detection stops |
| `turnStrategy.minWords` | 1 | integer | Transcripts shorter than this don't trigger a reply — filters one-word noise |
| `turnStrategy.useInterim` | `true` | boolean | Use interim transcripts for faster turn detection |

Suggested presets: **light** `0.75 / 0.65 / minWords 1` · **moderate**
`0.8 / 0.7 / minWords 2` · **aggressive** `0.85 / 0.75 / minWords 3`.

### Voicemail detection

```json
{
  "llmConfig": {
    "voicemailDetection": {
      "enabled": true,
      "action": "hangup",
      "leaveMessage": "",
      "language": "en",
      "gateTimeoutMs": 4000,
      "patternConfidence": 0.8
    }
  }
}
```

| Key | Default | Meaning |
|-----|---------|---------|
| `enabled` | `false` | Turn detection on |
| `action` | `"hangup"` | `hangup`, `leave_message`, or `callback` |
| `leaveMessage` | `""` | Spoken when `action` is `leave_message` |
| `language` | `"en"` | Pattern language: `en` or `hi` |
| `gateTimeoutMs` | `4000` | How long the agent's audio is held while detecting |
| `patternConfidence` | `0.8` | Minimum confidence to call it voicemail |

On native-audio providers, prefer prompt-based detection: instruct the agent to call
`end_call` with `disposition: "VOICEMAIL"`. It costs nothing and adds no latency.

### Built-in functions

```json
{ "llmConfig": { "enabledBuiltins": ["end_call", "finalize_conversation"] } }
```

`end_call` lets the agent hang up; `finalize_conversation` makes it write its
structured outcome before ending.

### Vertex AI (Gemini Live only)

Keyed by the provider id. Vertex is the default for Gemini Live because it is
substantially faster than AI Studio.

```json
{ "llmConfig": { "gemini-live-2.5": { "vertexai": { "enabled": true, "region": "us-central1" } } } }
```

Only `us-central1` is confirmed for Gemini Live 2.5.

## sttConfig and ttsConfig

Same rules as `llmConfig`: free-form, replaced wholesale, keys are provider-specific.
They carry tuning that has no dedicated column — for example:

```json
{ "ttsConfig": { "heypixa": { "voice": "kriti", "top_p": 0.95, "repetition_penalty": 1.3 } } }
```

Because these objects are unvalidated, treat them as advanced tuning: set the dedicated
fields (`ttsProvider`, `ttsVoice`, `sttModel`) first, and only reach for the config
objects when you need a provider option that has no field of its own.

## Next steps

- [Tools](https://edesy.in/docs/voice-agent/api/tools) — let this agent call your APIs mid-conversation
- [Calls](https://edesy.in/docs/voice-agent/api/calls) — place a call with this agent
- [Provider & Voice Catalog](https://edesy.in/docs/voice-agent/api/provider-catalog) — every valid provider, model, voice and language id
