Calls API

Place outbound and test calls, read call history, fetch a call's transcript, summary and recording, and receive a callback when a call ends.

Calls API

Place calls with an agent, then read back what happened — status, duration, recording, transcript, summary and extracted fields.

Method Path Purpose
POST /api/v1/calls Place an outbound call
GET /api/v1/calls Call history
GET /api/v1/calls/{conversationId} Full call detail
GET /api/v1/calls/{conversationId}/transcript Transcript only

Place a call

POST /api/v1/calls200 OK

This is the endpoint behind the dashboard's Test Call — the same orchestration, credit checks and backend selection.

curl -X POST https://voice-agent.edesy.in/api/v1/calls \
  -H "Authorization: Bearer $EDESY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agentId": 1042,
    "phoneNumber": "+919876543210",
    "variables": {
      "customer_name": "Rahul",
      "order_id": "ACM-88213"
    },
    "metadata": { "crm_lead_id": "L-5521" },
    "callbackUrl": "https://example.com/hooks/edesy/call-ended"
  }'
import os
import requests

response = requests.post(
    "https://voice-agent.edesy.in/api/v1/calls",
    headers={"Authorization": f"Bearer {os.environ['EDESY_API_KEY']}"},
    json={
        "agentId": 1042,
        "phoneNumber": "+919876543210",
        "variables": {"customer_name": "Rahul", "order_id": "ACM-88213"},
        "metadata": {"crm_lead_id": "L-5521"},
        "callbackUrl": "https://example.com/hooks/edesy/call-ended",
    },
    timeout=30,
)
response.raise_for_status()

call = response.json()["data"]
print(call["conversationId"])
const response = await fetch("https://voice-agent.edesy.in/api/v1/calls", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.EDESY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    agentId: 1042,
    phoneNumber: "+919876543210",
    variables: { customer_name: "Rahul", order_id: "ACM-88213" },
    metadata: { crm_lead_id: "L-5521" },
    callbackUrl: "https://example.com/hooks/edesy/call-ended",
  }),
});

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

const { data: call } = await response.json();
console.log(call.conversationId);
<?php
$ch = curl_init('https://voice-agent.edesy.in/api/v1/calls');
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([
        'agentId'     => 1042,
        'phoneNumber' => '+919876543210',
        'variables'   => ['customer_name' => 'Rahul', 'order_id' => 'ACM-88213'],
        'metadata'    => ['crm_lead_id' => 'L-5521'],
        'callbackUrl' => 'https://example.com/hooks/edesy/call-ended',
    ]),
]);

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

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

$call = json_decode($response, true)['data'];
echo $call['conversationId'];
{
  "success": true,
  "data": {
    "conversationId": "c3f1b6e2-8f5a-4a1e-9d21-7f0a2c4b9e11",
    "callSid": "CA9f0e1d2c3b4a5968778695a4b3c2d1e0",
    "status": "initiated"
  }
}

The response returns as soon as the call is dispatched to the carrier. Poll GET /api/v1/calls/{conversationId} or use callbackUrl for the outcome.

Request body

Field Type Required Notes
agentId integer Yes The agent that will handle the call
phoneNumber string Yes Destination. E.164 preferred
variables object Values for the agent's declared variables
metadata object Arbitrary data echoed back on the call record and callback
callbackUrl string (HTTPS) Notified when the call ends
fromNumber string Caller ID to present. Must be a number configured in your workspace
provider string Carrier override. Defaults to the agent's callProvider, then the workspace default
credentialId string Use a specific stored carrier credential
runtime v1 | v2 | v3 Voice stack override. Leave unset unless advised

Phone number normalisation. Numbers already carrying a country code (+… or 00…) are preserved exactly. A bare 10-digit Indian mobile (9876543210), an 11-digit trunk-prefixed one (09876543210), or a 12-digit 91… form are normalised to +91…. Anything else is passed through digits-only for the carrier to validate. Send E.164 for international destinations — an unprefixed number can be misrouted to the wrong country.

Errors

Status Code When
400 MISSING_AGENT_ID agentId absent
400 MISSING_PHONE_NUMBER phoneNumber absent
400 CALL_FAILED Agent not ready (no LLM provider or no prompt), or callbackUrl is not a valid HTTPS URL
402 CALL_FAILED Insufficient credits — the response includes the balance detail
429 CALL_FAILED Workspace concurrent-call limit reached
503 CALL_FAILED Voice backend unreachable
504 CALL_FAILED Backend timed out (30s)

Call history

GET /api/v1/calls

Query Type Description
agentId integer Filter by agent
status string completed, failed, dialing, not_connected
phoneNumber string Exact match on the recipient
startDate / endDate ISO date Range on call start time
limit / offset integer Pagination (max 100)
{
  "success": true,
  "data": {
    "calls": [
      {
        "conversationId": "c3f1b6e2-8f5a-4a1e-9d21-7f0a2c4b9e11",
        "callSid": "CA9f0e1d2c3b4a5968778695a4b3c2d1e0",
        "agentId": 1042,
        "agentName": "Order Status Bot",
        "phoneNumber": "+919876543210",
        "status": "completed",
        "duration": 74,
        "recordingUrl": "https://…",
        "source": "api",
        "startTime": "2026-08-10T09:31:02.000Z",
        "endTime": "2026-08-10T09:32:16.000Z",
        "metadata": { "crm_lead_id": "L-5521" }
      }
    ],
    "total": 128,
    "limit": 50,
    "offset": 0
  }
}

Results are newest-first by start time.

Get a call

GET /api/v1/calls/{conversationId}

curl https://voice-agent.edesy.in/api/v1/calls/$CONVERSATION_ID \
  -H "Authorization: Bearer $EDESY_API_KEY"
import os
import requests

response = requests.get(
    f"https://voice-agent.edesy.in/api/v1/calls/{conversation_id}",
    headers={"Authorization": f"Bearer {os.environ['EDESY_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()

call = response.json()["data"]
print(call["status"], call["duration"])
print(call["fullText"])
const response = await fetch(
  `https://voice-agent.edesy.in/api/v1/calls/${conversationId}`,
  { headers: { Authorization: `Bearer ${process.env.EDESY_API_KEY}` } }
);

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

const { data: call } = await response.json();
console.log(call.status, call.duration);
console.log(call.fullText);
<?php
$ch = curl_init("https://voice-agent.edesy.in/api/v1/calls/{$conversationId}");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('EDESY_API_KEY')],
]);

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

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

$call = json_decode($response, true)['data'];
echo $call['status'], ' ', $call['duration'], PHP_EOL;
echo $call['fullText'];

Returns everything from the list shape, plus:

Field Description
transcript Ordered turns: { role: "user" | "agent", text, timestamp }
fullText The transcript flattened to Agent: … / User: … lines
summary AI-generated post-call summary
disposition Call outcome classification — one of the values below
extraction Structured fields extracted from the conversation
recordingUrl Playable recording link, or null when there is no recording

disposition values. A fixed set:

Value Meaning
QUALIFIED The caller met your qualification criteria
NOT_QUALIFIED They did not
CALLBACK_REQUESTED They asked to be called back
NO_ANSWER Nobody picked up
VOICEMAIL Reached an answering machine
TRANSFERRED Handed off to a human
CUSTOM An outcome you defined yourself

extraction. An object whose keys are the extraction fields configured on the agent, so the shape is yours, not ours — an agent with order_id and issue_type fields yields {"order_id": "ACM-88213", "issue_type": "delivery_delay"}. It is null until post-call extraction runs, and stays null for calls with no transcript. Configure the fields in the dashboard under the agent's data fields.

summary, disposition and extraction are produced after the call ends. All three are always present in the response and are null until post-call analysis completes — treat null as "not ready yet", not as "no data". Poll for a few seconds, or use callbackUrl, rather than reading them straight after POST /calls returns.

404 NOT_FOUND if the conversation does not exist in your workspace.

Get a transcript

GET /api/v1/calls/{conversationId}/transcript

{
  "success": true,
  "data": {
    "conversationId": "c3f1b6e2-8f5a-4a1e-9d21-7f0a2c4b9e11",
    "transcript": [
      { "role": "agent", "text": "Namaste! Acme support…", "timestamp": "2026-08-10T09:31:04.220Z" },
      { "role": "user", "text": "Order ACM-88213", "timestamp": "2026-08-10T09:31:09.870Z" }
    ],
    "fullText": "Agent: Namaste! Acme support…\nUser: Order ACM-88213"
  }
}

Lighter than the full call detail — use it when you only need the conversation text.

Call-ended callback

Polling works, but a callback is better: you get told once, when the call is actually over.

Set callbackUrl on POST /api/v1/calls and we POST to it after the call ends. The URL must be HTTPS in production; an invalid URL is rejected at call-creation time rather than silently dropped.

This is not the same mechanism as Webhook Subscriptions. The per-call callback described here is unsigned, single-delivery, and emits only call.completed / call.failed / call.voicemail.

Payload

Content-Type: application/json, 15-second timeout.

{
  "event": "call.completed",
  "timestamp": "2026-08-10T09:32:16Z",
  "data": {
    "conversation_id": "c3f1b6e2-8f5a-4a1e-9d21-7f0a2c4b9e11",
    "call_sid": "CA9f0e1d2c3b4a5968778695a4b3c2d1e0",
    "phone_number": "+919876543210",
    "agent_id": 1042,
    "duration": 74,
    "disposition": "qualified",
    "transcript_summary": "Caller asked about order ACM-88213; confirmed delivery for Thursday.",
    "turn_count": 12,
    "recording_url": "https://…",
    "variables": { "customer_name": "Rahul", "order_id": "ACM-88213" }
  }
}
event Sent when
call.completed The call connected and finished normally
call.failed The call failed or went unanswered
call.voicemail An answering machine was reached

Notes that will save you a debugging session:

  • The payload is snake_case, unlike the REST responses, which are camelCase.
  • disposition here is lowercased (qualified, answered, no_answer), whereas GET /api/v1/calls/{id} returns it uppercased (QUALIFIED). Same concept, different casing — normalise before comparing.
  • variables echoes back what you sent on POST /api/v1/calls, which is the simplest way to correlate the callback with your own record.
  • Optional fields (recording_url, transcript_summary, turn_count, conversation_id, variables) are omitted entirely when empty, not sent as null. Read them defensively.

Delivery and verification

This callback is delivered once, unsigned, and unretried — treat it as a fast notification rather than a system of record. Two consequences:

  1. Verify out of band. On receipt, call GET /api/v1/calls/{conversation_id} with your API key and use that response as the truth. Don't act on the callback body alone, and don't expose the endpoint as if the payload were authenticated.
  2. Reconcile. If your endpoint is down or slow, that call's notification is lost. Periodically sweep GET /api/v1/calls?startDate=… for calls you never processed.

If you need signed, retried delivery, use Webhook Subscriptions instead.

A note on scopes

POST /api/v1/calls, GET /api/v1/calls, GET /api/v1/calls/{id} and GET /api/v1/calls/{id}/transcript currently require a valid API key and enforce workspace scoping, but do not yet enforce per-scope checks. Grant calls:read / calls:write on your keys anyway — scope enforcement will be applied to these endpoints, and keys already carrying the right scopes will keep working unchanged.

Next steps

  • Agents — configure the agent that handles the call
  • Tools — let the agent call your APIs mid-conversation
  • Webhook Subscriptions — signed, retried event delivery