AI Outbound Calling API
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
- Your system POSTs a call request to the webhook endpoint, authenticated with the campaign's API key.
- The request is validated and added to the campaign's outbound queue.
- 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.
- The AI agent converses with the called party using the per-call variables you supplied.
- 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.
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.
| Requirement | Why |
|---|---|
A campaign in active status | The API key belongs to a single campaign. A paused, completed, or archived campaign rejects requests (409). |
| A campaign API key | Authenticates your requests. Issued per campaign; regenerating it invalidates the old key immediately. |
| An AI agent + From DID on the campaign | The 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 account | The 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 account | Enables 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-wide | A 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.
- A missing or malformed
Authorizationheader returns401. - An unrecognized key returns
401. - Keep the key server-side. Regenerating it from the campaign edit form invalidates the previous key at once.
Queue an Outbound Call
Queue a single outbound AI call.
Endpoint
POST https://api.cloudspirevoice.com/agent/campaign-intake.php
Headers
| Header | Value |
|---|---|
Authorization | Bearer {campaign_api_key} (required) |
Content-Type | application/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
| Field | Type | Required | Description |
|---|---|---|---|
to_number | string | Yes | The 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_name | string | No | The called party's name, for display in the queue and reports. Truncated to 200 characters. |
variables | object | No | Per-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_id | string | No | Your 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_url | string | No | An https:// URL to receive the completion callback. Must be a valid https:// URL or the request is rejected with 400. Up to 500 characters. |
Success Response
HTTP 202 Accepted
{
"status": "queued",
"queue_id": 84213,
"campaign_id": 5,
"immediate": "dialing"
}
| Field | Type | Description |
|---|---|---|
status | string | Always "queued" on acceptance. The call has been recorded in the queue. |
queue_id | integer | The unique ID of the queue record. Reference it in support inquiries; it also appears in the completion callback. |
campaign_id | integer | The campaign the call was queued under (derived from your API key). |
immediate | string | The 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. |
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:
| Value | Meaning |
|---|---|
dialing | The call was placed immediately and is ringing. |
skipped_concurrency | At the campaign, tenant, or workspace concurrency ceiling. Stays queued; the worker places it when a slot frees. |
skipped_schedule | Outside the campaign's calling window. Stays queued; placed when the window reopens. |
skipped_no_plan | The account has no active AI outbound capability. Stays queued but will not dial until enabled. |
skipped_inactive | The campaign is not active. (You would normally get a 409 first; this can occur on a race.) |
retry_scheduled | The dial attempt hit a transient provider error; a retry was scheduled. |
failed | The inline attempt failed terminally (e.g. an invalid From DID configured on the campaign). |
Error Responses
| HTTP | Body error | Cause / Resolution |
|---|---|---|
400 | Invalid JSON request body | The body was not valid JSON. Send a well-formed JSON object with Content-Type: application/json. |
400 | Invalid 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. |
400 | webhook_response_url must be a valid https:// URL | The callback URL was present but not a valid https:// URL. Omit it or fix it. |
400 | variables payload too large (max 64KB) | The serialized variables object exceeds 64 KB. Send only what the agent prompt needs. |
401 | Missing or invalid Authorization header | No Authorization: Bearer ... header. Add it. |
401 | Invalid API key | The key is not recognized. Confirm it was not rotated. |
409 | Campaign is not active | The campaign is paused/completed/archived. The body includes campaign_status. Resume the campaign. |
409 | Number is blacklisted | to_number is on the tenant's blacklist phonebook. Remove it there to allow calls. |
503 | Outbound AI calling is currently disabled | The platform-wide master switch is off. Retry later or contact your administrator. |
500 | Internal server error | A 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.
| Status | Meaning | Terminal? |
|---|---|---|
queued | Accepted and awaiting placement (concurrency/schedule/plan permitting). Retried entries also return here until their next attempt time. | No |
dialing | The call has been placed and is ringing / in progress. | No |
completed | The call connected and ended normally (including a voicemail message left, when the campaign is set to do so). | Yes |
voicemail | Voicemail was reached and the campaign's voicemail action was to skip (or no retries remained). | Yes |
failed | The call could not be completed after exhausting retries, or hit a terminal condition (e.g. scam-block, no valid payment), or stalled without a result. | Yes |
blacklisted | The number was found on the tenant blacklist at placement time. | Yes |
Retry behavior is set on the campaign (max attempts and retry delay). Transient outcomes — no answer, busy, dial failed, provider error, and (when configured) voicemail — re-queue the call with a future attempt time until attempts are exhausted.
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.
Delivery Details
| Property | Value |
|---|---|
| Method | POST |
| Content-Type | application/json |
| Scheme | https:// only |
| Timeout | 5 seconds (3-second connect timeout) |
| Retries | None. The callback is fire-and-forget — deliver-once. Treat the queue view / reports as the source of truth, and 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,
"disconnection_reason": "agent_hangup",
"ai_summary": "Confirmed the appointment for March 20th at 2:30 PM with Dr. Smith.",
"ai_sentiment": "Positive",
"ai_successful": 1
}
Callback Fields
| Field | Type | Description |
|---|---|---|
queue_id | integer | The queue record ID returned when you queued the call. |
campaign_id | integer | The campaign that placed the call. |
external_id | string or null | The reference you supplied on the request. Use it to match the result to your record. |
to_number | string | The number that was called, in E.164. |
contact_name | string or null | The contact name you supplied. |
status | string | Terminal queue status: completed, voicemail, or failed. |
duration_sec | integer or null | Call duration in seconds (null if the call never connected). |
disconnection_reason | string | Why the call ended (e.g. agent_hangup, user_hangup, no_answer, voicemail_reached, busy, dial_failed). |
ai_summary | string or null | An AI-generated summary of the conversation. |
call_id | string or null | The 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_sentiment | string or null | Caller sentiment: Positive, Neutral, Negative, or Unknown. |
ai_successful | integer or null | Whether the agent judged the call's goal met: 1 (yes), 0 (no), or null (not determined). |
webhook_response_url, no callback is sent; retrieve results from the campaign's queue view and reports instead. Reconcile, don't rely: for calls carried by an externally-run AI provider (the dormant provider-hook surface), event ordering can skip the callback entirely — treat the queue view and reports as the authoritative outcome and use the callback as a convenience signal.
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}}.
- Use a flat object of string-friendly key/value pairs.
- Keep keys aligned with what the agent prompt expects — an unreferenced variable is simply ignored; a referenced-but-missing one renders empty.
- Do not put secrets in variables; they become part of the call context.
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:
- Track which records you have already submitted on your side, and POST each once.
- Set
external_idto your record's unique ID so the completion callback can be matched back without ambiguity. - On a
5xxor a network timeout where you did not receive a202, the request may still have been accepted and the call placed — reconcile against the queue view/reports before resubmitting (queueing is not idempotent; posting twice calls twice). On a4xx, fix the request first.
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 three ceilings, all configured by your administrator:
- Campaign concurrency — maximum simultaneous calls for this campaign.
- Tenant concurrency — maximum simultaneous AI outbound calls across all of the account's campaigns.
- Workspace threshold — a platform-wide safety ceiling on the underlying AI voice provider.
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.
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:
- Reads the records to call from your source system (authenticate to it, query the relevant entity).
- Maps each record to a request body:
to_numberin E.164, acontact_name, thevariablesthe agent prompt needs, andexternal_idset to the source record's unique ID. - POSTs each call to this endpoint with the campaign's Bearer key.
- 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
- Placed AI calls draw on the account's AI Voice Minutes pool (included minutes plus any overage) -- the same pool inbound AI voice uses. Outbound additionally requires the AI Outbound Calling add-on.
- Without both the AI Voice Minutes pool and the AI Outbound Calling add-on, requests are accepted (
202) but stay queued and are not dialed. - The
variablespayload is capped at 64 KB per call. - Numbers on the tenant blacklist are never dialed — they are rejected at submit time (
409) or markedblacklistedat placement.