CallCloudCallCloudAPI referenceGet an API key

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

  1. An admin creates a key in Settings, Developers, API keys. It is shown once, so copy it then.
  2. Check it works: GET /me returns your workspace, reps and call outcomes.
  3. 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.
  4. Sync results: poll GET /calls with the next_cursor from 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." } }
StatusCodeMeaning
400invalid_input, invalid_jsonThe body or a parameter is wrong; the message names the field.
401unauthorizedMissing or invalid key.
403not_in_beta, workspace_suspended, not_enabledThe API is off for this workspace, the workspace is suspended, or the feature (Sequences) is off.
404not_foundNo such list, campaign or call in this workspace.
429rate_limitedMore than 120 requests a minute on one key. Wait the seconds in the Retry-After header.
500internal_errorOur side. Safe to retry.

Limits: 120 requests per minute per key, 1,000 contacts per request, 30 custom fields per contact.

Your workspace

GET/me

The 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

GET/lists?limit=50

Your 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" } ] }
POST/lists

Create a list, optionally with up to 1,000 contacts in the same call.

FieldTypeDescription
namestring, required1 to 120 characters.
contactsarray, optionalUp 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

POST/lists/:id/contacts

Add 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.

FieldTypeDescription
contactsarray, required1 to 1,000 contact objects.

The response is { "list_id": "...", "import": { ... } } with the same import summary as above:

FieldMeaning
receivedContacts in the request.
createdNew prospects created.
linked_existingAlready a prospect in your workspace (same number), now also in this list.
dropped_no_phoneNo usable phone number.
dropped_toll_freeToll-free numbers are not dialed.
dropped_duplicateRepeated 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.

FieldTypeDescription
first_name, last_namestringUp to 300 characters each.
company, title, emailstringUp to 300 characters each.
phonestringPrimary number. E.164 (+15125550134) is safest; US numbers without +1 are accepted.
phonesarrayUp to 5 of { label: "mobile" | "home" | "office", number }.
notesstringUp to 5,000 characters, saved on the prospect.
external_idstringYour id for this person (a CRM record id). Returned on every call to them as prospect.external_id.
custom_fieldsobjectUp to 30 keys (60 characters each); values are strings, numbers or booleans. Saved on the prospect and returned on every call to them.

Campaigns

GET/campaigns

Your 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" } ] }
POST/campaigns/:id/lists

Add a list to the lists a campaign dials. It adds, never replaces, and is safe to repeat. It never starts, stops or changes dialing.

FieldTypeDescription
list_idstring, requiredA list in this workspace.
{ "campaign_id": "cmv...", "list_ids": ["cmv...", "cmv..."] }

Sync finished calls

GET/calls

Finished 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.

FieldTypeDescription
updated_sinceISO 8601 timeStart point when you have no cursor. Default: 24 hours ago.
cursorstringnext_cursor from your previous page. Takes precedence over updated_since.
limitnumber1 to 100 (default 50).
campaign_idstringOnly this campaign.
connected_onlytrueOnly 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

GET/calls/:id

Returns { "call": { ... } }: the call object plus transcript.

The call object

FieldTypeDescription
idstringThe call id.
created_at, updated_atISO timeWhen the call was placed, and when it last changed.
answered_at, ended_atISO time or nullWhen it was answered and when it ended.
directionstring"outbound" or "inbound".
campaignobject{ id, name }.
repobject or null{ id, name, email } of the rep on the call.
prospectobject{ id, external_id, first_name, last_name, company, title, email, phone, custom_fields }.
number_dialedstringThe number that was called, E.164.
resultstringWhat the line did, set by the dialer, for example connected, voicemail, no_answer or busy.
outcomeobject or null{ id, label } the rep picked, from your outcome list (GET /me).
ended_bystring or null"Rep hung up" or "Prospect hung up".
talk_secondsnumberSeconds the rep and prospect talked.
notesstring or nullThe rep's notes.
ai_summary, ai_next_stepsstring or nullAI summary and next steps, when AI summaries are on.
recording_url, recording_secondsstring or nullA link to the recording and its length, when the call was recorded.
transcriptstring or nullOnly on GET /calls/:id.

Hot leads: call a form fill now

POST/hot-leads

For 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.
FieldTypeDescription
campaign_idstring, requiredThe campaign to call them from (GET /campaigns).
contactobject, requiredA contact object with a phone number.
rep_emailstringSend 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.

Stats

GET/stats

Dials, 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.

FieldTypeDescription
period, from, to, timezonestringThe date range, as in GET /calls/search.
rep, campaign_idstringNarrow to one rep or campaign.
conversation_secondsnumberTalk 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

GET/prospects

Search prospects, most recently contacted first.

FieldTypeDescription
querystringName, company, email or phone.
statusstringnew, contacted, connected, callback, bad_number or dnc.
ownerstringOwner email or name.
limitnumber1 to 100 (default 25).
GET/prospects/:id

One 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.

GET/sequences

Your 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.

POST/files

Upload 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.

FieldTypeDescription
filenamestring, requiredThe name the prospect sees, for example Acme brochure.pdf.
content_base64stringThe file, base64. Send this or url.
urlstringA public https link to the file.
content_typestringFor example application/pdf. Guessed when left out.
POST/sequences/:id/emails

Push 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.

FieldTypeDescription
peoplearray, requiredOne object per person.
people[].prospect_id or emailstringWho it is for. A new prospect is created from email when there is no match.
people[].first_name, last_name, company, title, phone, notesstringUsed when a new prospect is created.
people[].subject, body, body_htmlstringThe email. Merge fields like {{first_name}} work; body_html wins over body.
people[].stepnumberWhich email step this is. Omit to fill email steps in order.
people[].file_idsarrayAttachments from POST /files, up to 10.
people[].emailsarraySeveral 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.