AI Outbound Calling API
Docs Home / API Reference / AI Outbound Calling API

AI Outbound Calling API

Queue calls that an AI agent places for you and receive each result at a callback URL.

The AI Outbound Calling API lets an external system (a CRM, scheduler, ERP, or any custom application) queue outbound calls that a Cloudspire AI agent places and conducts. You POST one request per call; the platform dials the number, the AI agent handles the conversation, and (optionally) the result is POSTed back to a callback URL of your choice when the call completes.

Calls route through the same PBX infrastructure as every other call on the platform, so they carry proper caller ID, STIR/SHAKEN attestation, and call recording.

How It Works

  1. Your system POSTs a call request to the webhook endpoint, authenticated with the campaign's API key.
  2. The request is validated and added to the campaign's outbound queue.
  3. The platform attempts to place the call immediately. If concurrency is exhausted or the request arrives outside the campaign's calling schedule, the call stays queued and a worker (running every minute) places it as soon as conditions allow. A request that carries call_at waits in the queue until that time instead (see Placing a Call at a Set Time).
  4. The AI agent converses with the called party using the per-call variables you supplied.
  5. When the call ends and is analyzed, the queue record is updated with the outcome. If you supplied a webhook_response_url, the result is POSTed there.
This endpoint is push-only. It receives call requests from your system; it does not poll or pull from your CRM/ERP. If your source system cannot POST out on its own, you will need a small connector that reads from your system and POSTs each call to this endpoint. See Integrating an External System below.

Prerequisites

Before your requests will result in placed calls, your Cloudspire administrator must have the following in place. Several of these are the reason a request can be accepted (queued) yet not dial.

RequirementWhy
A campaign in active statusThe API key belongs to a single campaign. A paused, completed, or archived campaign rejects requests (409).
A campaign API keyAuthenticates your requests. Issued per campaign; regenerating it invalidates the old key immediately.
An AI agent + From DID on the campaignThe agent conducts the call; the From DID is the caller ID. Both are configured on the campaign, not per request; you cannot choose the caller ID per call.
An AI Voice Minutes pool on the accountThe metered pool that outbound (and inbound) AI voice draws on. Required for any AI call to be placed.
The AI Outbound Calling add-on on the accountEnables the outbound system specifically (outbound needs both this AND the AI Voice Minutes pool). Without either, requests are still queued but never dialed (the inline result is skipped_no_plan).
Outbound AI calling enabled platform-wideA global master switch. When off, requests are rejected (503).

Base URL

https://api.cloudspirevoice.com/

Authentication

Authenticate every request with the campaign's API key as a Bearer token:

Authorization: Bearer {campaign_api_key}

The key maps the request to exactly one campaign, which determines the tenant, AI agent, From DID, schedule, concurrency limits, and retry behavior. There is no separate campaign ID to send; it is implied by the key.

Queue an Outbound Call

Queue a single outbound AI call.

Endpoint

POST https://api.cloudspirevoice.com/agent/campaign-intake.php

Headers

HeaderValue
AuthorizationBearer {campaign_api_key} (required)
Content-Typeapplication/json

Request Body

{
  "to_number": "+17705551234",
  "contact_name": "John Doe",
  "variables": {
    "appointment_date": "March 20th",
    "appointment_time": "2:30 PM",
    "provider": "Dr. Smith"
  },
  "external_id": "CRM-12345",
  "webhook_response_url": "https://your-system.example.com/cloudspire/callback"
}

Request Fields

FieldTypeRequiredDescription
to_numberstringYesThe number to call, in E.164 format (+17705551234). Numbers in a digits-only form with a country code are normalized, but E.164 is recommended. Rejected with 400 if missing or unparseable.
contact_namestringNoThe called party's name, for display in the queue and reports. Truncated to 200 characters.
variablesobjectNoPer-call dynamic variables passed to the AI agent (see Dynamic Variables). Flat key/value JSON. The serialized object must be under 64 KB, else 400.
external_idstringNoYour own reference for this call (e.g. a CRM record ID). Stored on the queue record and echoed back in the completion callback so you can reconcile results. Truncated to 255 characters.
webhook_response_urlstringNoAn https:// URL to receive the completion callback. Must be a valid https:// URL or the request is rejected with 400. Up to 500 characters.
call_atstringNoWhen to place the call, as an ISO 8601 date and time with its time zone offset, for example 2026-10-15T15:00:00-04:00 or 2026-10-15T19:00:00Z. Omit it to place the call now. A time without an offset, or a time in the past, is rejected with 400. See Placing a Call at a Set Time.

Success Response

HTTP 202 Accepted

{
  "status": "queued",
  "queue_id": 84213,
  "campaign_id": 5,
  "immediate": "dialing"
}
FieldTypeDescription
statusstringAlways "queued" on acceptance. The call has been recorded in the queue.
queue_idintegerThe unique ID of the queue record. Reference it in support inquiries; it also appears in the completion callback.
campaign_idintegerThe campaign the call was queued under (derived from your API key).
immediatestringThe outcome of the one inline placement attempt made during the request (see below). This is a hint, not a final status; queued calls are retried by the per-minute worker.
call_atstringPresent only when the request carried call_at: the time the call is held until, as the platform read it. Check it against the time you meant.
202 means accepted, not connected. A 202 confirms the call was queued. Whether it dials immediately depends on concurrency, schedule, and account state, reported in immediate. The authoritative outcome arrives later via the completion callback (or in the campaign's queue view).

The immediate Field

Common values:

ValueMeaning
dialingThe call was placed immediately and is ringing.
scheduledThe request carried call_at, so no call was placed now. It stays queued until that time.
skipped_concurrencyAt the campaign or account concurrency ceiling. Stays queued; the worker places it when a slot frees.
skipped_scheduleOutside the campaign's calling window. Stays queued; placed when the window reopens.
skipped_no_planThe account has no active AI outbound capability. Stays queued but will not dial until enabled.
skipped_inactiveThe campaign is not active. (You would normally get a 409 first; this can occur on a race.)
retry_scheduledThe dial attempt hit a transient provider error; a retry was scheduled.
failedThe inline attempt failed terminally (e.g. an invalid From DID configured on the campaign).

Error Responses

HTTPBody errorCause / Resolution
400Invalid JSON request bodyThe body was not valid JSON. Send a well-formed JSON object with Content-Type: application/json.
400Invalid or missing to_number (must be E.164 format)to_number is absent or not a dialable number. Use E.164 with a country code.
400webhook_response_url must be a valid https:// URLThe callback URL was present but not a valid https:// URL. Omit it or fix it.
400variables payload too large (max 64KB)The serialized variables object exceeds 64 KB. Send only what the agent prompt needs.
400call_at must be an ISO 8601 date and time with a time zone offset, ...call_at is not a date and time with an offset, or names a date that does not exist. Send a form like 2026-10-15T15:00:00-04:00.
400call_at is in the past; omit it to place the call nowThe time has already passed. Leave call_at out to call now, or send a later time.
401Missing or invalid Authorization headerNo Authorization: Bearer ... header. Add it.
401Invalid API keyThe key is not recognized. Confirm it was not rotated.
409Campaign is not activeThe campaign is paused/completed/archived. The body includes campaign_status. Resume the campaign.
409Number is blacklistedto_number is on the tenant's blacklist phonebook. Remove it there to allow calls.
503Outbound AI calling is currently disabledThe platform-wide master switch is off. Retry later or contact your administrator.
500Internal server errorA server-side error. Do not blind-retry: the queue row may already exist and the call may already be placed. Reconcile against the queue view first, then resubmit only if your reference is absent.

All error bodies are JSON of the form {"error": "..."}. The 409 Campaign is not active response additionally includes campaign_status.

Call Lifecycle & Queue Statuses

Each queued call moves through a series of statuses. You observe the final outcome via the completion callback; the intermediate states are visible in the campaign's queue view in the admin portal.

StatusMeaningTerminal?
queuedAccepted and awaiting placement (concurrency/schedule/plan permitting). A request with call_at waits here until that time, and retried entries return here until their next attempt time; the queue view shows that time under the status.No
dialingThe call has been placed and is ringing / in progress.No
completedThe call connected and ended normally.Yes
voicemailVoicemail answered on the final attempt: either the campaign's voicemail message was left, or no message was set to be left and no retry remained.Yes
failedNo attempt was answered (no answer, busy, or the dial failed) by the time attempts ran out; the final attempt was answered but nothing was accomplished (a carrier recording, a silent line) or, with Retry When set to Call didn’t finish, the called party cut it short; or the call hit a terminal condition (e.g. a refused destination) or stalled without a result.Yes
cancelledCancelled from the campaign’s queue view before its next attempt: by hand, with Cancel queued calls, or by pausing the campaign with its Pause button. No callback is sent; the result lookup shows it.Yes
blacklistedThe number was found on the tenant blacklist at placement time.Yes

Retry behavior is set on the campaign (max attempts and retry delay). An attempt that is not answered, reaches voicemail (when the campaign retries on voicemail), or is answered but accomplishes nothing re-queues the call for another attempt after the retry delay. With the campaign’s Retry When set to Call didn’t finish, a conversation the called party cut short is retried too. The final attempt is never re-queued: it produces the result, so the result of a request that keeps failing arrives with its last attempt; with 3 attempts 30 minutes apart, about an hour after the first dial. One exception: when the last attempt could not be placed at all (the dial itself failed), the request is marked failed one retry delay later.

Pausing a campaign. A paused campaign refuses new requests (409). Calls already queued wait and dial as soon as the campaign is active again, unless they are cancelled: the Pause button cancels them as it pauses, and a paused campaign’s queue view offers Cancel queued calls. Setting the status to paused on the campaign form keeps them.

Completion Callback

If you include a webhook_response_url on the request, Cloudspire POSTs the call result to that URL once the call reaches a terminal status (completed, voicemail, or failed) and post-call analysis has finished. This is how you get the transcript summary and sentiment back into your system.

Testing the callback before your endpoint exists. Ask Cloudspire support for your agent’s Test receiver address (staff issue it, because it carries the agent’s platform token): an address on this platform that records whatever is posted to it and acts on nothing. Send it as the webhook_response_url on a test call, with any query string you plan to use, and the delivery appears under Deliveries to the test receiver on the agent’s Tools card with the address exactly as it was dialed (credential values redacted), the headers and the body. If the agent also has a custom integration pointed at the same address, the two deliveries for one call show the same call_id. The receiver keeps only the newest deliveries for each agent, so it is for proving the contract, not for collecting results.

Delivery Details

PropertyValue
AddressThe webhook_response_url exactly as you sent it on that request, query string included. Redirects are not followed, so give the final address.
MethodPOST
AuthenticationNone added by the platform: the callback carries no custom headers and no signature. To make sure a callback came from us, put a secret token of your own in the query string of each webhook_response_url and check it on arrival.
Content-Typeapplication/json
Schemehttps:// only
Timeout5 seconds (3-second connect timeout)
RetriesNone. The callback is fire-and-forget, deliver-once. If one is missed, look the result up; have your endpoint respond quickly with a 2xx.

Callback Payload

{
  "queue_id": 84213,
  "campaign_id": 5,
  "external_id": "CRM-12345",
  "to_number": "+17705551234",
  "contact_name": "John Doe",
  "status": "completed",
  "duration_sec": 92,
  "seconds_to_answer": 11,
  "disconnection_reason": "agent_hangup",
  "ai_summary": "Confirmed the appointment for March 20th at 2:30 PM with Dr. Smith.",
  "ai_sentiment": "Positive",
  "ai_successful": true,
  "call_id": "sip1.atl.cloudspirevoice.com-1789678613.22316"
}

One Callback per Request

Each request produces one completion callback, sent when the request reaches its terminal status, however many attempts that took. When an attempt connects, the callback follows that attempt (after its analysis) and carries that attempt’s call_id. When no attempt connects, the callback follows the last attempt, with call_id null and the outcome in status and disconnection_reason. When the final attempt was answered but did not succeed (a carrier recording, a silent line), the callback follows that attempt like any answered call, with its call_id and status failed. Attempts that are retried send nothing, and a request cancelled before its final attempt sends none at all (the result lookup shows it as cancelled). Your external_id is on every callback. In the rare case of a platform restart between sending and recording a callback, the same callback can be sent a second time, so treat queue_id as the de-duplication key.

Callback Fields

FieldTypeDescription
queue_idintegerThe queue record ID returned when you queued the call.
campaign_idintegerThe campaign that placed the call.
external_idstring or nullThe reference you supplied on the request. Use it to match the result to your record.
to_numberstringThe number that was called, in E.164.
contact_namestring or nullThe contact name you supplied.
statusstringTerminal queue status: completed, voicemail, or failed.
duration_secinteger or nullCall duration in seconds (null if the call never connected).
seconds_to_answerinteger or nullSeconds from dialing to the call being answered, taken from the carrier’s record of the attempt that produced this result. A phone that rings usually takes ten seconds or more to answer. An answer within two or three seconds, especially with voicemail_reached, usually means the carrier sent the call straight to voicemail without ringing the phone, as some carriers do when their call-screening filters divert a number. Use it to spot a number being diverted rather than a person who did not pick up. Null when that attempt was not answered, and on a request cancelled before its final attempt, which has no final attempt to measure. The carrier record is written shortly after the call ends; if a callback is ever sent before it is written, the field is null in that callback and the result lookup below returns it.
disconnection_reasonstring or nullWhy the answered call ended; see Disconnection Reasons. Null when no attempt was answered (no answer, busy, or a failed dial).
ai_summarystring or nullAn AI-generated summary of the conversation.
call_idstring or nullThe per-call identifier of the attempt that produced this result. The same value arrives as call.call_id in any mid-call custom integration request made during the call, so you can join the two. Null when the call never connected.
ai_sentimentstring or nullCaller sentiment: Positive, Neutral, Negative, or Unknown.
ai_successfulboolean or nullWhether the agent judged the call's goal met: true, false, or null (not determined, including every call that never connected).
Because the callback fires only after analysis completes, expect it seconds to a minute or two after the call ends, not the instant the line drops. If you supplied no webhook_response_url, no callback is sent; use the result lookup or the campaign's queue view and reports instead.

Disconnection Reasons

disconnection_reason says how an answered call ended. Read the outcome from status; the reason adds detail, and new values can be added, so handle a value you do not recognize by its status.

ValueUsual statusMeaning
agent_hangupcompletedThe agent finished the conversation and ended the call.
agent_transfer, agent_handoffcompletedThe agent transferred the call to a person, another destination, or another agent.
agent_voicemailvoicemailThe agent put the called party through to a voicemail box to leave a message.
caller_hangup, eofcompleted, or failed when the final attempt accomplished nothing or was cut short (see the retry rules above)The called party (or their carrier) ended the call. A short carrier recording that hangs up on its own before the agent can respond ends this way.
silence_timeoutcompleted, or failed when nothing was accomplishedThe line was answered but stayed silent.
max_duration_reachedcompletedThe call reached the agent’s maximum call length.
voicemail_reachedvoicemailVoicemail answered and no message was left.
voicemail_drop_playedvoicemailVoicemail answered and the campaign’s voicemail message was left.
voicemail_drop_truncatedfailedThe voicemail message was cut off before it finished, so it counts as not delivered (and is retried while attempts remain).
unreachablefailedThe agent recognized that the number cannot take the call: a mailbox that is not set up or is full, a disconnected-number recording, or an automated system it cannot get past.
Other values with status failedfailedA technical failure on the call (for example read_error or tts_no_audio). Worth reporting to support if you see them repeatedly.

Looking Up a Result

The callback is sent once and is not retried, so if your endpoint was unreachable when it fired, ask for the result instead. The lookup uses the same campaign API key as the intake, reads only your own campaign’s requests, places no call, and answers whatever the campaign’s status is.

GET https://api.cloudspirevoice.com/agent/campaign-result.php?external_id=CRM-12345
GET https://api.cloudspirevoice.com/agent/campaign-result.php?queue_id=84213
Authorization: Bearer {campaign_api_key}

Pass exactly one of external_id (your reference) or queue_id (the id returned in the 202). The response carries the same fields as the completion callback, plus attempts, created_at, completed_at and terminal (false while the request is still queued, dialing or connected, so you can tell a result from a request in progress):

{
  "campaign_id": 5,
  "count": 1,
  "results": [{
    "queue_id": 84213,
    "call_id": "sip1.atl.cloudspirevoice.com-1789678613.22316",
    "campaign_id": 5,
    "external_id": "CRM-12345",
    "to_number": "+17705551234",
    "contact_name": "John Doe",
    "status": "completed",
    "terminal": true,
    "attempts": 1,
    "duration_sec": 92,
    "seconds_to_answer": 11,
    "disconnection_reason": "agent_hangup",
    "ai_summary": "Confirmed the appointment for March 20th at 2:30 PM with Dr. Smith.",
    "ai_sentiment": "Positive",
    "ai_successful": true,
    "created_at": "2026-03-18 14:02:11",
    "completed_at": "2026-03-18 14:04:03"
  }]
}

Because the intake does not de-duplicate on external_id, a lookup by external_id returns every request that carried it, newest first (up to 10). ai_summary and the other analysis fields fill in a short while after the call ends, exactly when the callback would have been sent. Errors: 400 when neither or both parameters are passed, 401 for a missing or unknown key, 404 when your campaign has no request with that reference. That includes a lookup made before the request was submitted: a reference exists only once its POST has returned 202, so start looking after the 202, or read a 404 as “not submitted yet”.

The lookup is the fallback for a callback you missed, not a live feed. Checking every 30 to 60 seconds is plenty. Posting the same external_id again places a new call, so do not re-submit a request to find out how it went.

Dynamic Variables

The variables object carries per-call values into the AI agent's prompt. The campaign can define default variables; your per-call variables are merged over those defaults (per-call wins on a key clash). Reference them in the agent prompt with double-brace syntax, e.g. {{appointment_date}}.

Idempotency & Reconciliation

Each accepted POST creates exactly one queue entry and one call attempt sequence. The endpoint does not de-duplicate on external_id or to_number: if you POST the same record twice, the number is called twice. To stay idempotent:

Concurrency & Scheduling

You can POST calls faster than they are dialed; the queue absorbs the burst and the worker drains it within the configured limits. Placement is bounded by two ceilings, both configured by your administrator:

If each result you write back uses a resource your side limits (a licensed login, an API rate limit), keep the campaign concurrency at or below that limit.

Calls submitted outside the campaign's scheduled calling window (days of week and a daily start/end time in the campaign's timezone) remain queued and are placed automatically when the next window opens. There is no need to time your POSTs to the window; load the queue whenever it suits your system.

Placing a Call at a Set Time

To have a call placed later, for example at the time a contact asked to be called back, send call_at with the request. The request is accepted with a 202 as usual, immediate reads scheduled, and the response echoes the call_at it will use. The call waits in the queue and the per-minute worker places it on its first run at or after that time, so it rings within about a minute of the time you asked for.

Getting Calls Answered

Mobile carriers screen incoming calls with call-protection services that score each calling number. A number with a low score can be labeled as spam or diverted without ringing the phone at all. In that case the carrier answers the call itself within two or three seconds and plays the person’s voicemail greeting, so the result reads voicemail even though the phone never rang. Four things help:

To catch a number being diverted, read seconds_to_answer: a voicemail result, or a failed one, answered within two or three seconds usually means the carrier took the call instead of ringing the phone.

Integrating an External System (CRM / ERP)

Because the endpoint is push-only, integrating a system that exposes its own API (for example, an ERP you query for the day's appointments) means writing a small connector that:

  1. Reads the records to call from your source system (authenticate to it, query the relevant entity).
  2. Maps each record to a request body: to_number in E.164, a contact_name, the variables the agent prompt needs, and external_id set to the source record's unique ID.
  3. POSTs each call to this endpoint with the campaign's Bearer key.
  4. Consumes the completion callback (or polls your own results store) and writes the outcome back to the source record, matched on external_id.

The connector holds the credentials for both sides and owns the business logic of which records to call and when. Cloudspire's responsibility begins at the POST and ends at the callback.

Example: queue a call with curl

curl -X POST https://api.cloudspirevoice.com/agent/campaign-intake.php \
  -H "Authorization: Bearer {campaign_api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "to_number": "+17705551234",
    "contact_name": "John Doe",
    "variables": { "appointment_date": "March 20th", "appointment_time": "2:30 PM" },
    "external_id": "CRM-12345",
    "webhook_response_url": "https://your-system.example.com/cloudspire/callback"
  }'
HTTP/1.1 202 Accepted
{ "status": "queued", "queue_id": 84213, "campaign_id": 5, "immediate": "dialing" }

Billing & Limits