# API Overview

> Base URL, authentication, scopes, rate limits, response envelope and error codes for the Edesy Voice AI REST API, plus an index of every endpoint.

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

The Edesy Voice AI REST API lets you build and configure agents, give them tools,
place calls, and read back transcripts and outcomes — everything the dashboard does
for agent configuration and call placement, backed by the same services.

- **Base URL:** `https://voice-agent.edesy.in`
- **All endpoints are under:** `/api/v1`
- **Machine-readable spec:** `GET https://voice-agent.edesy.in/api/v1/openapi.json` (no auth required)

The OpenAPI document is generated from the running code at request time, so it cannot
disagree with the deployed API. Use it to generate a typed client in your language of
choice.

## Endpoint index

| What you want to do | Endpoint | Page |
|---------------------|----------|------|
| Create an agent | `POST /api/v1/agents` | [Agents](https://edesy.in/docs/voice-agent/api/agents) |
| Update an agent, or any part of its configuration | `PATCH /api/v1/agents/{id}` | [Agents](https://edesy.in/docs/voice-agent/api/agents) |
| List / read / delete agents | `GET`, `DELETE /api/v1/agents[/{id}]` | [Agents](https://edesy.in/docs/voice-agent/api/agents) |
| Add a tool to an agent | `POST /api/v1/functions` | [Tools](https://edesy.in/docs/voice-agent/api/tools) |
| Update a tool | `PATCH /api/v1/functions/{id}` | [Tools](https://edesy.in/docs/voice-agent/api/tools) |
| List / read / delete tools | `GET`, `DELETE /api/v1/functions[/{id}]` | [Tools](https://edesy.in/docs/voice-agent/api/tools) |
| Place an outbound or test call | `POST /api/v1/calls` | [Calls](https://edesy.in/docs/voice-agent/api/calls) |
| Call history | `GET /api/v1/calls` | [Calls](https://edesy.in/docs/voice-agent/api/calls) |
| Individual call details, transcript, recording | `GET /api/v1/calls/{id}` | [Calls](https://edesy.in/docs/voice-agent/api/calls) |
| Be notified when a call ends | `callbackUrl` on `POST /api/v1/calls` | [Calls](https://edesy.in/docs/voice-agent/api/calls) |
| Campaign progress, and how many numbers were called | `GET /api/v1/campaigns[/{id}]` | [Campaigns](https://edesy.in/docs/voice-agent/api/campaigns) |
| Outcome of every contact in a campaign | `GET /api/v1/campaigns/{id}/calls` | [Campaigns](https://edesy.in/docs/voice-agent/api/campaigns) |
| Valid provider, model, voice and language ids | — | [Provider & Voice Catalog](https://edesy.in/docs/voice-agent/api/provider-catalog) |

Also available under `/api/v1`: `phone-numbers`, `knowledge-base` and
`workflows/{id}/trigger`. See the OpenAPI spec for their shapes.

## Authentication

Every request carries a **server API key**. Create one in the dashboard under
**Settings → API Keys**. The key is displayed **once** at creation and cannot be
recovered afterwards — store it in your secrets manager immediately.

| Prefix | Purpose |
|--------|---------|
| `vp_live_…` | Production credentials |
| `vp_test_…` | Non-production credentials |

Either header works:

```bash
curl https://voice-agent.edesy.in/api/v1/agents \
  -H "Authorization: Bearer vp_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

# or
curl https://voice-agent.edesy.in/api/v1/agents \
  -H "X-API-Key: vp_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
```

> **Server-side only.** A `vp_` key grants full workspace access at its scopes. Never
> ship it to a browser, mobile app, or anything a user can inspect — call the API from
> your own backend and proxy the results to your front end.

### Scopes

A key is granted a set of scopes at creation. Grant the narrowest set that works — a
reporting integration should not be able to delete an agent.

| Scope | Grants |
|-------|--------|
| `agents:read` | List / read agents |
| `agents:write` | Create, update, delete agents |
| `functions:read` | List / read tools |
| `functions:write` | Create, update, delete tools |
| `calls:read` | Read calls and transcripts |
| `calls:write` | Place outbound calls |
| `phone_numbers:read` / `phone_numbers:write` | Manage phone numbers |
| `knowledge_base:read` / `knowledge_base:write` | Manage knowledge bases |
| `workflows:read` / `workflows:write` | Manage workflows |

Wildcards are supported: `agents:*` grants both agent scopes, and `*` grants
everything. Note that **write does not imply read** — grant both if you need both.

A key with **no** scopes authenticates successfully but is authorized for nothing.

### Rate limits

Default **120 requests/minute per key** (configurable per key). Every response carries
the current state:

| Header | Meaning |
|--------|---------|
| `X-RateLimit-Limit` | Requests allowed per minute |
| `X-RateLimit-Remaining` | Requests left in the current window |
| `X-RateLimit-Reset` | Unix seconds when the window resets |
| `Retry-After` | Seconds to wait (present only on `429`) |

Back off using these headers rather than parsing error bodies.

### Rotation

Rotating a key from the dashboard mints the replacement **first** and puts the old key
on a grace deadline (24 hours by default), so you can redeploy without an outage
window. Revoking is immediate — use it for a leaked credential.

## Conventions

### Response envelope

Success:

```json
{ "success": true, "data": {} }
```

Failure:

```json
{
  "success": false,
  "error": "Human-readable message",
  "code": "MACHINE_READABLE_CODE",
  "details": []
}
```

Always branch on the HTTP status or `success`. `code` is stable and safe to switch on;
`error` is for humans and may be reworded. `details` is present on `VALIDATION_ERROR`
and contains the per-field issues.

### Pagination

List endpoints accept `limit` (default `50`, max `100`) and `offset` (default `0`), and
return `total` alongside the results.

### Timestamps

ISO-8601 UTC strings. Agent objects expose `createdAt` / `updatedAt`.

### Errors common to every endpoint

| Status | Code | When |
|--------|------|------|
| 401 | `MISSING_API_KEY` | No `Authorization` or `X-API-Key` header |
| 401 | `INVALID_KEY_FORMAT` | Not a well-formed `vp_live_` / `vp_test_` key |
| 401 | `INVALID_API_KEY` | Key does not match any active credential |
| 401 | `KEY_REVOKED` | Key was revoked |
| 401 | `KEY_EXPIRED` | Key expired, or its rotation grace period ended |
| 403 | `INSUFFICIENT_SCOPE` | Key lacks the scope this operation requires |
| 429 | `RATE_LIMIT_EXCEEDED` | Per-key rate limit exceeded — see `Retry-After` |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

Every key is bound to exactly one workspace. Requesting a resource that belongs to a
different workspace returns `404 NOT_FOUND`, never another tenant's data.

## Quick start

Create an agent, give it a tool, place a call, and read the transcript.

<!-- tabs -->

```bash
export EDESY_API_KEY="vp_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
export BASE="https://voice-agent.edesy.in/api/v1"

# 1. Create the agent
AGENT_ID=$(curl -sS -X POST "$BASE/agents" \
  -H "Authorization: Bearer $EDESY_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "Order Status Bot",
    "language": "hindi_english",
    "prompt": "You are Acme support. Ask for the order number, call lookup_order, and read back the status.",
    "greetingMessage": "Namaste! Acme support. Aapka order number bataiye.",
    "llmProvider": "gemini-live-2.5",
    "callProvider": "twilio"
  }' | jq -r '.data.id')

# 2. Attach a tool
curl -sS -X POST "$BASE/functions" \
  -H "Authorization: Bearer $EDESY_API_KEY" -H "Content-Type: application/json" \
  -d "{
    \"agentId\": $AGENT_ID,
    \"name\": \"lookup_order\",
    \"description\": \"Look up an order's status. Call as soon as the customer gives an order number.\",
    \"parametersSchema\": { \"order_id\": { \"type\": \"string\", \"description\": \"Order number\" } },
    \"requiredParams\": [\"order_id\"],
    \"httpMethod\": \"POST\",
    \"httpUrl\": \"https://api.example.com/orders/lookup\",
    \"httpHeaders\": { \"Content-Type\": \"application/json\" },
    \"httpBody\": \"{\\\"orderId\\\": \\\"{{order_id}}\\\"}\"
  }"

# 3. Place a call
CONV_ID=$(curl -sS -X POST "$BASE/calls" \
  -H "Authorization: Bearer $EDESY_API_KEY" -H "Content-Type: application/json" \
  -d "{ \"agentId\": $AGENT_ID, \"phoneNumber\": \"+919876543210\" }" \
  | jq -r '.data.conversationId')

# 4. After the call ends, read the transcript
curl -sS "$BASE/calls/$CONV_ID" \
  -H "Authorization: Bearer $EDESY_API_KEY" | jq '.data | {status, duration, summary, fullText}'
```

```python
import os
import requests

BASE = "https://voice-agent.edesy.in/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['EDESY_API_KEY']}"


def post(path, payload):
    r = session.post(f"{BASE}{path}", json=payload, timeout=30)
    r.raise_for_status()
    return r.json()["data"]


# 1. Create the agent
agent = post("/agents", {
    "name": "Order Status Bot",
    "language": "hindi_english",
    "prompt": "You are Acme support. Ask for the order number, call lookup_order, and read back the status.",
    "greetingMessage": "Namaste! Acme support. Aapka order number bataiye.",
    "llmProvider": "gemini-live-2.5",
    "callProvider": "twilio",
})

# 2. Attach a tool
post("/functions", {
    "agentId": agent["id"],
    "name": "lookup_order",
    "description": "Look up an order's status. Call as soon as the customer gives an order number.",
    "parametersSchema": {"order_id": {"type": "string", "description": "Order number"}},
    "requiredParams": ["order_id"],
    "httpMethod": "POST",
    "httpUrl": "https://api.example.com/orders/lookup",
    "httpHeaders": {"Content-Type": "application/json"},
    "httpBody": '{"orderId": "{{order_id}}"}',
})

# 3. Place a call
call = post("/calls", {"agentId": agent["id"], "phoneNumber": "+919876543210"})

# 4. After the call ends, read the transcript
r = session.get(f"{BASE}/calls/{call['conversationId']}", timeout=30)
r.raise_for_status()
result = r.json()["data"]
print(result["status"], result["duration"], result["summary"])
print(result["fullText"])
```

```javascript
const BASE = "https://voice-agent.edesy.in/api/v1";
const headers = {
  Authorization: `Bearer ${process.env.EDESY_API_KEY}`,
  "Content-Type": "application/json",
};

async function call(path, method = "GET", body) {
  const res = await fetch(`${BASE}${path}`, {
    method,
    headers,
    ...(body ? { body: JSON.stringify(body) } : {}),
  });
  if (!res.ok) throw new Error(`Edesy API ${res.status}: ${await res.text()}`);
  return (await res.json()).data;
}

// 1. Create the agent
const agent = await call("/agents", "POST", {
  name: "Order Status Bot",
  language: "hindi_english",
  prompt:
    "You are Acme support. Ask for the order number, call lookup_order, and read back the status.",
  greetingMessage: "Namaste! Acme support. Aapka order number bataiye.",
  llmProvider: "gemini-live-2.5",
  callProvider: "twilio",
});

// 2. Attach a tool
await call("/functions", "POST", {
  agentId: agent.id,
  name: "lookup_order",
  description:
    "Look up an order's status. Call as soon as the customer gives an order number.",
  parametersSchema: {
    order_id: { type: "string", description: "Order number" },
  },
  requiredParams: ["order_id"],
  httpMethod: "POST",
  httpUrl: "https://api.example.com/orders/lookup",
  httpHeaders: { "Content-Type": "application/json" },
  httpBody: '{"orderId": "{{order_id}}"}',
});

// 3. Place a call
const placed = await call("/calls", "POST", {
  agentId: agent.id,
  phoneNumber: "+919876543210",
});

// 4. After the call ends, read the transcript
const result = await call(`/calls/${placed.conversationId}`);
console.log(result.status, result.duration, result.summary);
console.log(result.fullText);
```

```php
<?php
const BASE = 'https://voice-agent.edesy.in/api/v1';

function edesy(string $path, string $method = 'GET', ?array $body = null): array {
    $ch = curl_init(BASE . $path);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . getenv('EDESY_API_KEY'),
            'Content-Type: application/json',
        ],
    ]);
    if ($body !== null) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    }

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

    if ($status >= 400) {
        throw new RuntimeException("Edesy API {$status}: {$response}");
    }
    return json_decode($response, true)['data'];
}

// 1. Create the agent
$agent = edesy('/agents', 'POST', [
    'name'            => 'Order Status Bot',
    'language'        => 'hindi_english',
    'prompt'          => 'You are Acme support. Ask for the order number, call lookup_order, and read back the status.',
    'greetingMessage' => 'Namaste! Acme support. Aapka order number bataiye.',
    'llmProvider'     => 'gemini-live-2.5',
    'callProvider'    => 'twilio',
]);

// 2. Attach a tool
edesy('/functions', 'POST', [
    'agentId'          => $agent['id'],
    'name'             => 'lookup_order',
    'description'      => "Look up an order's status. Call as soon as the customer gives an order number.",
    'parametersSchema' => ['order_id' => ['type' => 'string', 'description' => 'Order number']],
    'requiredParams'   => ['order_id'],
    'httpMethod'       => 'POST',
    'httpUrl'          => 'https://api.example.com/orders/lookup',
    'httpHeaders'      => ['Content-Type' => 'application/json'],
    'httpBody'         => '{"orderId": "{{order_id}}"}',
]);

// 3. Place a call
$call = edesy('/calls', 'POST', [
    'agentId'     => $agent['id'],
    'phoneNumber' => '+919876543210',
]);

// 4. After the call ends, read the transcript
$result = edesy('/calls/' . $call['conversationId']);
echo $result['status'], ' ', $result['duration'], ' ', $result['summary'], PHP_EOL;
echo $result['fullText'];
```

<!-- /tabs -->

## Practical notes

**Idempotency.** `POST /api/v1/calls` is not idempotent — a retry places a second call.
Retry only on `429`, `503` and `504`, with backoff, and de-duplicate on your side using
`metadata`.

**Partial updates.** Both `PATCH` endpoints write only the fields you send. To clear a
nullable field send `null`; omitting it leaves it unchanged.

**Object-valued fields.** `llmConfig`, `sttConfig`, `ttsConfig` and `variables` are
replaced wholesale, not deep-merged. Read, modify, write back.

**Caching.** Agent and tool writes invalidate the voice backends' caches synchronously,
so a `2xx` means the change is live for the next call.

**Multi-tenancy.** A key can only ever reach its own workspace. Ids belonging to another
workspace return `404`, not `403`, so ids cannot be probed.

## Next steps

- [Agents](https://edesy.in/docs/voice-agent/api/agents) — create and configure agents
- [Tools](https://edesy.in/docs/voice-agent/api/tools) — let an agent call your APIs mid-conversation
- [Calls](https://edesy.in/docs/voice-agent/api/calls) — place calls, read history and transcripts
- [Campaigns](https://edesy.in/docs/voice-agent/api/campaigns) — campaign progress and per-contact outcomes
- [Provider & Voice Catalog](https://edesy.in/docs/voice-agent/api/provider-catalog) — every valid provider, model, voice and language id
