CallCloud API
Push prospects into the lists your campaigns dial, and pull every finished call, with its outcome, notes, recording and AI summary, back into your CRM. JSON over HTTPS.
Everything the CallCloud MCP server can do, this API can do too: the same stats, call search, prospects and sequence emails, answered by the same code.
Getting started
- An admin creates a key in Settings, Developers, API keys. It is shown once, so copy it then.
- Check it works:
GET /mereturns your workspace, reps and call outcomes. - Feed a campaign: create a list with contacts (
POST /lists), then attach it to a campaign (POST /campaigns/:id/lists). Campaigns are created and started in the app. - Sync results: poll
GET /callswith thenext_cursorfrom your last page.
Base URL: https://dialer.callcloud.app/api/v1
Authentication
Send your key as a bearer token on every request. Keys start with ccp_, belong to one workspace, and must stay on your server, never in a browser.
curl https://dialer.callcloud.app/api/v1/me \
-H "Authorization: Bearer ccp_your_key"Errors and limits
Every error has the same shape:
{ "error": { "code": "not_found", "message": "No list with id \"abc\" in this workspace." } }| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_input, invalid_json | The body or a parameter is wrong; the message names the field. |
| 401 | unauthorized | Missing or invalid key. |
| 403 | not_in_beta, workspace_suspended, not_enabled | The API is off for this workspace, the workspace is suspended, or the feature (Sequences) is off. |
| 404 | not_found | No such list, campaign or call in this workspace. |
| 429 | rate_limited | More than 120 requests a minute on one key. Wait the seconds in the Retry-After header. |
| 500 | internal_error | Our side. Safe to retry. |
Limits: 120 requests per minute per key, 1,000 contacts per request, 30 custom fields per contact.
Your workspace
/meThe workspace this key belongs to, its active reps, and its call outcome list. A call's outcome.id is one of these ids.
{
"workspace": { "id": "cmv...", "name": "Acme Sales" },
"reps": [ { "id": "cmv...", "name": "Dana R.", "email": "dana@acme.com", "role": "admin" } ],
"outcomes": [ { "id": "meeting_booked", "label": "Meeting booked", "kind": "..." } ]
}Lists
/lists?limit=50Your lead lists, newest first. limit is 1 to 200 (default 50).
{ "lists": [ { "id": "cmv...", "name": "October webinar", "prospects": 412, "campaign_ids": ["cmv..."], "created_at": "2026-10-20T14:02:11.000Z" } ] }/listsCreate a list, optionally with up to 1,000 contacts in the same call.
| Field | Type | Description |
|---|---|---|
name | string, required | 1 to 120 characters. |
contacts | array, optional | Up to 1,000 contact objects. |
curl -X POST https://dialer.callcloud.app/api/v1/lists \
-H "Authorization: Bearer ccp_your_key" -H "Content-Type: application/json" \
-d '{"name":"October webinar","contacts":[{"first_name":"Dana","last_name":"Reyes","company":"Acme","phone":"+15125550134","external_id":"crm_8812"}]}'{
"list": { "id": "cmv...", "name": "October webinar", "created_at": "2026-10-20T14:02:11.000Z" },
"import": { "received": 1, "created": 1, "linked_existing": 0, "dropped_no_phone": 0, "dropped_toll_free": 0, "dropped_duplicate": 0 }
}Add contacts to a list
/lists/:id/contactsAdd 1 to 1,000 contacts to an existing list. Safe to resend: a contact already in the list counts as a duplicate, not a second prospect. A campaign dialing this list picks the new prospects up on its next fill, running or not.
| Field | Type | Description |
|---|---|---|
contacts | array, required | 1 to 1,000 contact objects. |
The response is { "list_id": "...", "import": { ... } } with the same import summary as above:
| Field | Meaning |
|---|---|
received | Contacts in the request. |
created | New prospects created. |
linked_existing | Already a prospect in your workspace (same number), now also in this list. |
dropped_no_phone | No usable phone number. |
dropped_toll_free | Toll-free numbers are not dialed. |
dropped_duplicate | Repeated in the request or already in this list. |
The contact object
Every field is optional except that a contact needs a phone number to be dialed. Prospects are matched across your workspace on their primary number.
| Field | Type | Description |
|---|---|---|
first_name, last_name | string | Up to 300 characters each. |
company, title, email | string | Up to 300 characters each. |
phone | string | Primary number. E.164 (+15125550134) is safest; US numbers without +1 are accepted. |
phones | array | Up to 5 of { label: "mobile" | "home" | "office", number }. |
notes | string | Up to 5,000 characters, saved on the prospect. |
external_id | string | Your id for this person (a CRM record id). Returned on every call to them as prospect.external_id. |
custom_fields | object | Up to 30 keys (60 characters each); values are strings, numbers or booleans. Saved on the prospect and returned on every call to them. |
Campaigns
/campaignsYour campaigns (archived ones left out) and the lists each one dials.
{ "campaigns": [ { "id": "cmv...", "name": "Inbound leads", "status": "dialing", "list_ids": ["cmv..."], "created_at": "2026-10-01T15:20:00.000Z" } ] }/campaigns/:id/listsAdd a list to the lists a campaign dials. It adds, never replaces, and is safe to repeat. It never starts, stops or changes dialing.
| Field | Type | Description |
|---|---|---|
list_id | string, required | A list in this workspace. |
{ "campaign_id": "cmv...", "list_ids": ["cmv...", "cmv..."] }Sync finished calls
/callsFinished calls, oldest change first, built for syncing into a CRM. A call shows up again when its outcome or notes change after it ends, so keep polling with your last next_cursor to get only what changed.
| Field | Type | Description |
|---|---|---|
updated_since | ISO 8601 time | Start point when you have no cursor. Default: 24 hours ago. |
cursor | string | next_cursor from your previous page. Takes precedence over updated_since. |
limit | number | 1 to 100 (default 50). |
campaign_id | string | Only this campaign. |
connected_only | true | Only calls where someone picked up and talked to the rep. |
curl "https://dialer.callcloud.app/api/v1/calls?limit=100&connected_only=true" \
-H "Authorization: Bearer ccp_your_key"{ "calls": [ { ...call object... } ], "next_cursor": "MTc2MDk3...", "has_more": false }next_cursor is kept on the last page too: poll later with it and you get only newer changes. Page until has_more is false.
One call, with transcript
/calls/:idReturns { "call": { ... } }: the call object plus transcript.
The call object
| Field | Type | Description |
|---|---|---|
id | string | The call id. |
created_at, updated_at | ISO time | When the call was placed, and when it last changed. |
answered_at, ended_at | ISO time or null | When it was answered and when it ended. |
direction | string | "outbound" or "inbound". |
campaign | object | { id, name }. |
rep | object or null | { id, name, email } of the rep on the call. |
prospect | object | { id, external_id, first_name, last_name, company, title, email, phone, custom_fields }. |
number_dialed | string | The number that was called, E.164. |
result | string | What the line did, set by the dialer, for example connected, voicemail, no_answer or busy. |
outcome | object or null | { id, label } the rep picked, from your outcome list (GET /me). |
ended_by | string or null | "Rep hung up" or "Prospect hung up". |
talk_seconds | number | Seconds the rep and prospect talked. |
notes | string or null | The rep's notes. |
ai_summary, ai_next_steps | string or null | AI summary and next steps, when AI summaries are on. |
recording_url, recording_seconds | string or null | A link to the recording and its length, when the call was recorded. |
transcript | string or null | Only on GET /calls/:id. |
Hot leads: call a form fill now
/hot-leadsFor warm leads that should be called the moment they arrive, like a web form fill. The contact joins the campaign, then:
- Someone is dialing that campaign and free: one rep (the one you name, else whoever has waited longest) has their ringing lines stopped, and the lead is dialed alone on their line with a Hot lead notice. Calls already in progress are never cut. Their normal dialing resumes after the call, and the lines that were stopped are called again later.
- Nobody is free: the lead goes to the front of the campaign's queue and your workspace admins get an email. It is the first call when someone presses Start.
| Field | Type | Description |
|---|---|---|
campaign_id | string, required | The campaign to call them from (GET /campaigns). |
contact | object, required | A contact object with a phone number. |
rep_email | string | Send it to this rep when they are dialing and free. |
curl -X POST https://dialer.callcloud.app/api/v1/hot-leads \
-H "Authorization: Bearer ccp_your_key" -H "Content-Type: application/json" \
-d '{"campaign_id":"CAMPAIGN_ID","rep_email":"dana@acme.com","contact":{"first_name":"Sam","last_name":"Lee","phone":"+15125550134","notes":"Requested a demo on the pricing page"}}'{ "prospect_id": "cmv...", "status": "dialing", "rep": { "email": "dana@acme.com" }, "import": { ... } }status is dialing (with the rep) or queued. A Do Not Call person answers 409 do_not_call. Calling hours still apply: outside them the lead is queued.
Search calls
/calls/searchThe call log with filters, newest first, with the AI summary and rep notes on each. Use it to answer questions; use GET /calls to sync everything into a CRM.
| Field | Type | Description |
|---|---|---|
period | string | today, yesterday, 7d, 30d or 90d (default 7d). Ignored when from or to is given. |
from, to | YYYY-MM-DD | First and last day, inclusive, read in timezone. |
timezone | string | IANA timezone, for example America/New_York. |
rep | string | A team member: their email, or part of their name. |
campaign_id | string | Only this campaign. |
search | string | Prospect name, company, email or phone. |
connected_only | true | Only calls marked connected. |
disposition | string | Exact call result, for example connected, voicemail_left, no_answer, busy, bad_number. |
outcome | string | An outcome label, for example Meeting booked. |
min_talk_seconds | number | Only calls at least this long. |
with_transcript, with_recording | true | Only calls that have one. |
limit, page | number | limit 1 to 100 (default 25); page from 1. |
Stats
/statsDials, pickups, connects, conversations, meetings and talk time for a date range, broken down by rep, campaign, day, call result and outcome. Same definitions as the Analytics page.
| Field | Type | Description |
|---|---|---|
period, from, to, timezone | string | The date range, as in GET /calls/search. |
rep, campaign_id | string | Narrow to one rep or campaign. |
conversation_seconds | number | Talk time a connect needs to count as a conversation. Default 30. |
curl "https://dialer.callcloud.app/api/v1/stats?period=30d&rep=dana@acme.com" \
-H "Authorization: Bearer ccp_your_key"Prospects
/prospectsSearch prospects, most recently contacted first.
| Field | Type | Description |
|---|---|---|
query | string | Name, company, email or phone. |
status | string | new, contacted, connected, callback, bad_number or dnc. |
owner | string | Owner email or name. |
limit | number | 1 to 100 (default 25). |
/prospects/:idOne prospect: details, notes, imported fields, LinkedIn, and their last 20 calls with summaries.
Sequences
Available when Sequences is on for your workspace; otherwise these answer 403 not_enabled.
/sequencesYour email sequences: id, on or off, sending inboxes and window, each step (number, type, delay, subject), and how many people are in each status. Add ?include_archived=true for archived ones.
/filesUpload an attachment, for example a custom brochure, for emails you push. Send the bytes as content_base64 or give an https url to fetch it from; up to 10 MB. Returns a file_id.
| Field | Type | Description |
|---|---|---|
filename | string, required | The name the prospect sees, for example Acme brochure.pdf. |
content_base64 | string | The file, base64. Send this or url. |
url | string | A public https link to the file. |
content_type | string | For example application/pdf. Guessed when left out. |
/sequences/:id/emailsPush each person's own email, with attachments, into a sequence. Each person is matched to a prospect (by prospect_id, else email) or created, added to the sequence, and their email replaces that step's template copy for them only. Sending follows the sequence: its inboxes, window, pacing and stop-on-reply. Pushing again before the step runs replaces their email. Up to 50 people per call.
| Field | Type | Description |
|---|---|---|
people | array, required | One object per person. |
people[].prospect_id or email | string | Who it is for. A new prospect is created from email when there is no match. |
people[].first_name, last_name, company, title, phone, notes | string | Used when a new prospect is created. |
people[].subject, body, body_html | string | The email. Merge fields like {{first_name}} work; body_html wins over body. |
people[].step | number | Which email step this is. Omit to fill email steps in order. |
people[].file_ids | array | Attachments from POST /files, up to 10. |
people[].emails | array | Several emails for one person, one per email step, each with the fields above. |
curl -X POST https://dialer.callcloud.app/api/v1/sequences/SEQUENCE_ID/emails \
-H "Authorization: Bearer ccp_your_key" -H "Content-Type: application/json" \
-d '{"people":[{"email":"dana@acme.com","first_name":"Dana","subject":"Your custom plan","body":"Hi {{first_name}}, the plan we discussed is attached.","file_ids":["FILE_ID"]}]}'Questions or something missing? Email richard@callcloud.app.