Campaigns API
Track your campaigns from your own systems: how far a campaign has got, how many numbers were actually called, and what happened on each one.
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/campaigns |
List campaigns with progress counters |
GET |
/api/v1/campaigns/{id} |
One campaign, with exact per-status contact counts |
GET |
/api/v1/campaigns/{id}/calls |
Every contact in the campaign, with its call outcome |
All three need a key with the calls:read scope. See API Overview
for authentication, rate limits and the response envelope.
Read-only. Creating, starting, pausing and stopping campaigns stays in the dashboard. Starting a campaign dials real people and spends credits, so it goes through the dashboard's confirmation step rather than a single API call.
List campaigns
GET /api/v1/campaigns
| Query | Type | Description |
|---|---|---|
status |
string | DRAFT, SCHEDULED, RUNNING, PAUSED, COMPLETED, CANCELLED |
agentId |
integer | Only campaigns run by this agent |
limit / offset |
integer | Pagination (default 50, max 100) |
Results are ordered newest first.
curl "https://voice-agent.edesy.in/api/v1/campaigns?status=RUNNING" \
-H "Authorization: Bearer $EDESY_API_KEY"
{
"success": true,
"data": {
"campaigns": [
{
"id": 1208,
"name": "October renewals",
"description": null,
"status": "RUNNING",
"agentId": 1042,
"contactListId": 311,
"provider": "edesy",
"scheduledAt": null,
"startedAt": "2026-09-29T04:30:00.000Z",
"completedAt": null,
"totalContacts": 1500,
"callsCompleted": 1200,
"callsSuccessful": 950,
"callsFailed": 250,
"callsPending": 300,
"maxParallelCalls": 10,
"callRatePerMinute": 30,
"retryEnabled": true,
"maxRetryAttempts": 2,
"createdAt": "2026-09-28T11:02:14.000Z",
"updatedAt": "2026-09-29T06:10:41.000Z"
}
],
"total": 1,
"limit": 50,
"offset": 0
}
}
Progress counters
| Field | Meaning |
|---|---|
totalContacts |
Contacts in the campaign |
callsCompleted |
Contacts processed so far. Includes failed calls: it is callsSuccessful + callsFailed, plus any blocked numbers that were skipped |
callsSuccessful |
Calls that connected |
callsFailed |
Calls that did not connect (busy, no answer, invalid number, …) |
callsPending |
Contacts not yet processed |
These counters update live while the campaign runs. When you need an exact figure at a
point in time, use callStats from Get a campaign.
Get a campaign
GET /api/v1/campaigns/{id}
Returns every field from the list endpoint plus callStats, which is counted directly
from the campaign's contacts when you make the request. Use it to answer "how many
numbers were called?"
curl "https://voice-agent.edesy.in/api/v1/campaigns/1208" \
-H "Authorization: Bearer $EDESY_API_KEY"
import os
import requests
response = requests.get(
"https://voice-agent.edesy.in/api/v1/campaigns/1208",
headers={"Authorization": f"Bearer {os.environ['EDESY_API_KEY']}"},
timeout=30,
)
response.raise_for_status()
stats = response.json()["data"]["callStats"]
print(f"{stats['dialledContacts']} of {stats['totalContacts']} numbers called")
const response = await fetch("https://voice-agent.edesy.in/api/v1/campaigns/1208", {
headers: { Authorization: `Bearer ${process.env.EDESY_API_KEY}` },
});
if (!response.ok) {
throw new Error(`Edesy API ${response.status}: ${await response.text()}`);
}
const { data: campaign } = await response.json();
const { dialledContacts, totalContacts } = campaign.callStats;
console.log(`${dialledContacts} of ${totalContacts} numbers called`);
<?php
$ch = curl_init('https://voice-agent.edesy.in/api/v1/campaigns/1208');
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}");
}
$stats = json_decode($response, true)['data']['callStats'];
echo "{$stats['dialledContacts']} of {$stats['totalContacts']} numbers called";
{
"success": true,
"data": {
"id": 1208,
"name": "October renewals",
"status": "RUNNING",
"totalContacts": 1500,
"callsCompleted": 1200,
"callsSuccessful": 950,
"callsFailed": 250,
"callsPending": 300,
"startedAt": "2026-09-29T04:30:00.000Z",
"completedAt": null,
"callStats": {
"totalContacts": 1500,
"dialledContacts": 1260,
"byStatus": {
"PENDING": 240,
"CALLING": 4,
"COMPLETED": 950,
"FAILED": 236,
"RETRY": 60,
"CANCELLED": 10
}
}
}
}
The response also carries every other field shown under List campaigns.
callStats
| Field | Meaning |
|---|---|
totalContacts |
Contacts in the campaign |
dialledContacts |
Contacts that were actually dialled at least once |
byStatus |
Contacts in each status. Every status is always present, 0 when none |
Errors
| Status | Code | When |
|---|---|---|
| 400 | INVALID_CAMPAIGN_ID |
{id} is not an integer |
| 404 | NOT_FOUND |
No campaign with this id in your workspace |
List the calls in a campaign
GET /api/v1/campaigns/{id}/calls
One row per contact in the campaign, including contacts that have not been dialled yet. This is the endpoint to sync campaign outcomes into a CRM.
| Query | Type | Description |
|---|---|---|
status |
string | Only contacts in this status |
limit / offset |
integer | Pagination (default 50, max 100) |
Rows are ordered by id, oldest contact first, so paging with offset stays stable
while the campaign is still dialling.
curl "https://voice-agent.edesy.in/api/v1/campaigns/1208/calls?status=COMPLETED&limit=100" \
-H "Authorization: Bearer $EDESY_API_KEY"
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 campaign_calls(campaign_id, status=None):
"""Yield every contact in a campaign, one page at a time."""
offset = 0
while True:
params = {"limit": 100, "offset": offset}
if status:
params["status"] = status
r = session.get(f"{BASE}/campaigns/{campaign_id}/calls", params=params, timeout=30)
r.raise_for_status()
page = r.json()["data"]
yield from page["calls"]
offset += len(page["calls"])
if offset >= page["total"] or not page["calls"]:
break
for call in campaign_calls(1208):
print(call["phoneNumber"], call["status"], call["disposition"], call["durationSec"])
const BASE = "https://voice-agent.edesy.in/api/v1";
const headers = { Authorization: `Bearer ${process.env.EDESY_API_KEY}` };
async function* campaignCalls(campaignId, status) {
let offset = 0;
while (true) {
const qs = new URLSearchParams({ limit: "100", offset: String(offset) });
if (status) qs.set("status", status);
const res = await fetch(`${BASE}/campaigns/${campaignId}/calls?${qs}`, { headers });
if (!res.ok) throw new Error(`Edesy API ${res.status}: ${await res.text()}`);
const { data } = await res.json();
yield* data.calls;
offset += data.calls.length;
if (offset >= data.total || data.calls.length === 0) break;
}
}
for await (const call of campaignCalls(1208)) {
console.log(call.phoneNumber, call.status, call.disposition, call.durationSec);
}
<?php
function campaign_calls(int $campaignId, ?string $status = null): Generator {
$offset = 0;
while (true) {
$query = ['limit' => 100, 'offset' => $offset];
if ($status !== null) {
$query['status'] = $status;
}
$ch = curl_init("https://voice-agent.edesy.in/api/v1/campaigns/{$campaignId}/calls?" . http_build_query($query));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('EDESY_API_KEY')],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new RuntimeException("Edesy API {$httpCode}: {$response}");
}
$page = json_decode($response, true)['data'];
yield from $page['calls'];
$offset += count($page['calls']);
if ($offset >= $page['total'] || count($page['calls']) === 0) {
break;
}
}
}
foreach (campaign_calls(1208) as $call) {
echo "{$call['phoneNumber']} {$call['status']} {$call['disposition']}", PHP_EOL;
}
{
"success": true,
"data": {
"calls": [
{
"id": 88121,
"phoneNumber": "+919876543210",
"contactName": "Asha",
"status": "COMPLETED",
"attemptNumber": 1,
"lastAttemptAt": "2026-09-29T05:31:00.000Z",
"nextRetryAt": null,
"callStartedAt": "2026-09-29T05:31:02.000Z",
"callCompletedAt": "2026-09-29T05:32:36.000Z",
"durationSec": 94,
"disposition": "QUALIFIED",
"customDisposition": null,
"endReason": "end_call",
"errorMessage": null,
"fromNumber": "+918035000000",
"conversationId": "9adb70562f38fca1",
"callSummary": "Customer confirmed renewal for next month.",
"extractedData": { "renewal_month": "November" }
}
],
"total": 950,
"limit": 100,
"offset": 0
}
}
Call fields
| Field | Type | Description |
|---|---|---|
id |
integer | This contact's id within the campaign |
phoneNumber |
string | Number dialled (E.164) |
contactName |
string | null | Name from your contact list |
status |
string | Contact status |
attemptNumber |
integer | 1 for the first dial, increasing with each retry |
lastAttemptAt |
datetime | null | When the latest attempt was made |
nextRetryAt |
datetime | null | When the next retry is scheduled, if any |
callStartedAt / callCompletedAt |
datetime | null | Start and end of the call |
durationSec |
integer | null | Call length in seconds |
disposition |
string | null | The outcome. For connected calls, what the agent recorded (e.g. QUALIFIED, CALLBACK_REQUESTED, NOT_INTERESTED). For calls that never connected, the carrier's outcome (e.g. BUSY, NO_ANSWER, WRONG_NUMBER) |
customDisposition |
string | null | Your agent's custom outcome label, if one was used |
endReason |
string | null | Why the call ended |
errorMessage |
string | null | Failure reason for calls that did not connect |
fromNumber |
string | null | Caller ID used |
conversationId |
string | null | Use with GET /api/v1/calls/{conversationId} for the transcript and recording. null if the call never connected |
callSummary |
string | null | AI summary, when post-call analysis is enabled |
extractedData |
object | null | Fields extracted after the call, when post-call extraction is enabled |
Filtered status vs returned status. The
statusfilter matches the status the dialler recorded. Occasionally a call is recorded asFAILEDeven though a conversation took place; that row is returned asCOMPLETED, with its real duration and outcome. So?status=FAILEDcan include a few rows that showCOMPLETED.
Errors
| Status | Code | When |
|---|---|---|
| 400 | INVALID_CAMPAIGN_ID |
{id} is not an integer |
| 400 | INVALID_STATUS |
status is not one of the contact statuses |
| 404 | NOT_FOUND |
No campaign with this id in your workspace |
Contact statuses
| Status | Meaning |
|---|---|
PENDING |
Not dialled yet |
CALLING |
Call in progress |
COMPLETED |
Call connected and finished |
FAILED |
Did not connect, and no retries are left |
RETRY |
Did not connect; another attempt is scheduled (see nextRetryAt) |
CANCELLED |
Will not be dialled, e.g. the number is on your block list or the campaign was cancelled |
Generating a client
All three endpoints are in the OpenAPI spec at
GET https://voice-agent.edesy.in/api/v1/openapi.json (operations listCampaigns,
getCampaign and listCampaignCalls), so you can generate a typed client in any
language.
Next steps
- Calls: transcript, recording and summary for a single call
- Webhook Subscriptions: get pushed each outcome as it happens instead of polling
- Campaigns guide: create and run campaigns in the dashboard