Campaigns API

Read campaign progress, count how many numbers were called, and pull the outcome of every contact in a campaign into your CRM.

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 status filter matches the status the dialler recorded. Occasionally a call is recorded as FAILED even though a conversation took place; that row is returned as COMPLETED, with its real duration and outcome. So ?status=FAILED can include a few rows that show COMPLETED.

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