External AI Provider Hooks
These endpoints let an externally managed AI provider — a hosted AI voice/chat platform that runs the conversation on its own infrastructure — interface with Cloudspire. Through them the provider receives per-call context, reports call lifecycle events (which drive call history, campaign results, and notifications platform-side), and invokes PBX tools mid-call (directory lookups, availability checks, message taking, CRM actions).
Base URL
https://api.cloudspirevoice.com/
Endpoint Overview
| Endpoint | Direction | Purpose | Authentication |
|---|---|---|---|
/external-ai/webhook.php | Provider → Cloudspire | Call/chat lifecycle events | Webhook signature header (IP allowlist fallback) |
/external-ai/dynamic-vars.php | Provider → Cloudspire | Inbound-call dynamic variables | None (echo-only) |
/ai/pbx-tools.php | Provider → Cloudspire | Mid-call PBX / CRM tool calls | Engine API token (Bearer header preferred; ?token= legacy) |
/external-ai/admin-tools.php | Provider → Cloudspire | Internal assistant tools (platform system agents only) | Engine API token (query param or Bearer) |
/agent/campaign-intake.php | Your system → Cloudspire | Queue outbound AI calls | Campaign API key (Bearer) |
Call Lifecycle Webhook
The single webhook endpoint for all provider events. The provider POSTs one event per state change of a voice call or chat session; Cloudspire validates the signature, resolves the agent, logs the event to the AI call history, and runs the platform-side effects for that event type — outbound-campaign result attribution, result-webhook fan-out, demo summary emails, and critical-error alerting.
Endpoint
POST https://api.cloudspirevoice.com/external-ai/webhook.php
Authentication
Requests are authenticated by a signature header:
x-retell-signature: v={timestamp_ms},d={hmac_sha256_hex}
The digest is HMAC-SHA256(webhook_signing_key, raw_request_body + timestamp_ms) over the raw POST body exactly as sent, with the timestamp appended as a decimal string of milliseconds. The signing key is the provider integration's webhook key, configured platform-side by your Cloudspire administrator (a platform-level setting, not per customer). A signature older than the configured maximum age (default 300 seconds) is rejected.
- When a webhook key is configured, a missing or invalid signature causes the event to be discarded. The endpoint still returns
200— deliberate, so a misconfigured provider does not retry-storm the platform. Watch the AI logs, not the HTTP status, when debugging signature failures. - When no webhook key is configured, the endpoint falls back to a source-IP allowlist of the provider's egress ranges (also administrator-configured). Off-allowlist requests are discarded, again with
200. - The event's
agent_idmust resolve to a configured, active AI agent; events for unknown or inactive agents are acknowledged (200) and ignored.
Event Types
Every accepted event is written to the AI call history (visible in the admin portal's AI request logs) with the call/chat ID, numbers, duration, cost, and disconnection reason. In addition, specific events trigger platform-side processing:
| Event | Channel | Platform-side effect |
|---|---|---|
call_started | voice | Logged to call history. |
call_ended | voice | Logged with duration, disconnection reason, and cost. If the call was placed by the AI outbound campaign system (its metadata carries the queue reference), the queue record is updated: terminal status (completed / voicemail / failed) or an automatic retry is scheduled per the campaign's retry and voicemail policy, and the transcript is stored (recording URLs are not persisted -- see the field table below). |
call_analyzed | voice | Logged with sentiment and summary. Result-webhook deliveries are queued to the customer's configured result webhook URLs (requires the API webhook capability in the customer's plan). For outbound-campaign calls, the AI summary, sentiment, and success flag are written to the queue record, and the campaign's completion callback fires if the call has already reached a terminal status -- when call_analyzed arrives before call_ended (an ordering this legacy endpoint permits), no callback is sent for that call. For demo agents, one consolidated demo summary email is sent (summary, sentiment, caller, duration, plus any structured tool submissions captured during the call). |
transcript_updated | voice | Logged to call history. |
transfer_started / transfer_bridged / transfer_cancelled / transfer_ended | voice | Logged with the transfer destination. |
chat_started / chat_ended / chat_analyzed | chat | Legacy, no longer resolvable: provider chat-agent identifiers cannot be mapped to a platform agent any more (chat runs on the internal engine), so these events are acknowledged with 200 and discarded. This webhook is effectively voice-scoped. |
Independently of the event type, an event whose disconnection_reason is one of the critical values concurrency_limit_reached, no_valid_payment, scam_detected, or error_retell triggers an immediate alert email to the platform's configured alert address.
Request Body
Voice events carry the session under a call object; chat events carry a chat object with the analogous field names (chat_id, chat_status, chat_type, chat_analysis, chat_cost).
{
"event": "call_ended",
"call": {
"call_id": "call_abc123",
"agent_id": "agent_xyz789",
"call_type": "phone_call",
"from_number": "+17705551234",
"to_number": "+14045556789",
"call_status": "ended",
"start_timestamp": 1767000000000,
"end_timestamp": 1767000092000,
"disconnection_reason": "user_hangup",
"transcript": "...",
"recording_url": "https://...",
"metadata": {},
"call_analysis": {
"call_summary": "...",
"user_sentiment": "Positive",
"call_successful": true,
"in_voicemail": false
},
"call_cost": { "combined_cost": 42 }
}
}
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
event | string | Yes | One of the event types above. |
call.call_id | string | Yes | The provider's unique session ID (chat.chat_id for chat). Used as the correlation ID across history, tool submissions, and campaign records. |
call.agent_id | string | Yes | The provider-side agent ID configured on the Cloudspire AI agent. Events without it (or with an unknown value) are acknowledged and ignored. |
call.call_type | string | No | Provider call type (e.g. phone_call). Logged. |
call.from_number / call.to_number | string | No | Caller / called number in E.164. Logged. |
call.start_timestamp / call.end_timestamp | integer | No | Unix milliseconds. Their difference is the recorded duration. |
call.disconnection_reason | string | No | Why the session ended (e.g. user_hangup, agent_hangup, no_answer, busy, dial_failed, voicemail_reached). Drives outbound-queue status mapping and critical alerting. |
call.transfer_destination | string | No | Destination of a transfer event. Logged. |
call.transcript | string | No | Full transcript; stored on outbound-campaign queue records at call_ended. |
call.recording_url | string | No | Accepted but currently ignored — the platform does not persist provider recording URLs (recordings for platform-run calls are captured platform-side). |
call.metadata | object | No | Free-form session metadata set at call creation. For outbound campaign calls it carries the queue reference that ties the event back to the queue record (validated against the stored session ID — spoofed metadata is ignored). |
call.call_analysis | object | No | Post-call analysis: call_summary (string), user_sentiment (string), call_successful (boolean), in_voicemail (boolean). Chat uses chat_analysis with chat_summary. |
call.call_cost | object | No | {"combined_cost": <cents>}. Logged; a zero/absent cost on chat_ended / chat_analyzed marks the session as empty and removes it from the logs. |
Responses
| HTTP | Body | Meaning |
|---|---|---|
200 | {"status": "ok"} or empty | Acknowledged, not guaranteed processed. 200 is returned for accepted events, for events discarded on a failed signature or unknown agent (empty body, deliberately 200 to suppress provider retries), and when a platform-side side effect (history write, queue update, notification) fails after acceptance — such failures are logged and alarmed platform-side rather than surfaced to the caller, and the provider must not retry. There is no durable-processing contract on this legacy endpoint. |
400 | {"error": "..."} | Empty body or invalid JSON. |
405 | — | Method other than POST. |
500 | — | Platform configuration could not be loaded. Safe to retry. |
Example
curl -X POST https://api.cloudspirevoice.com/external-ai/webhook.php \
-H "Content-Type: application/json" \
-H "x-retell-signature: v={timestamp_ms},d={hmac_sha256_hex}" \
-d '{
"event": "call_ended",
"call": {
"call_id": "call_abc123",
"agent_id": "agent_xyz789",
"from_number": "+17705551234",
"to_number": "+14045556789",
"start_timestamp": 1767000000000,
"end_timestamp": 1767000092000,
"disconnection_reason": "user_hangup",
"call_cost": { "combined_cost": 42 }
}
}'
HTTP/1.1 200 OK
{ "status": "ok" }
Inbound Dynamic Variables Webhook
Called by the provider before it answers an inbound call. The provider POSTs the caller information it has; Cloudspire returns the dynamic-variable map the provider substitutes into the agent's prompt (double-brace syntax, e.g. {{from_number}}). The endpoint echoes the caller and called numbers back in the provider's nested response contract.
Endpoint
POST https://api.cloudspirevoice.com/external-ai/dynamic-vars.php
Authentication
None. The endpoint performs no lookups and returns only values echoed from the request itself, so it carries no credential requirement.
Request Body
{
"event": "call_inbound",
"call_inbound": {
"agent_id": "agent_xyz789",
"from_number": "+17705551234",
"to_number": "+14045556789"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
event | string | No | call_inbound in the provider's contract; not evaluated. |
call_inbound.agent_id | string | No | The provider agent taking the call; accepted, not evaluated. |
call_inbound.from_number | string | No | Caller's number. Echoed into the variable map (empty string when absent). |
call_inbound.to_number | string | No | Called number. Echoed into the variable map (empty string when absent). |
A legacy flat body (the same fields at the top level, without the call_inbound wrapper) is also accepted.
Response
The variable map contains exactly two keys: from_number and to_number.
{
"call_inbound": {
"dynamic_variables": {
"from_number": "+17705551234",
"to_number": "+14045556789"
}
}
}
Errors: 400 {"error": "Invalid JSON"} for an unparseable body; 405 for a method other than POST.
Example
curl -X POST https://api.cloudspirevoice.com/external-ai/dynamic-vars.php \
-H "Content-Type: application/json" \
-d '{
"event": "call_inbound",
"call_inbound": {
"agent_id": "agent_xyz789",
"from_number": "+17705551234",
"to_number": "+14045556789"
}
}'
PBX Tools API
The provider-agnostic mid-call tool endpoint. During a live conversation, the provider's LLM invokes custom tools by POSTing here; each call returns a short text result the AI reads (or paraphrases) to the caller. The tools query and act on the agent's own tenant — company directory, business hours, departments, presence, caller lookup, queue status, message taking, and (when licensed) the tenant's connected CRM.
Endpoint
POST https://api.cloudspirevoice.com/ai/pbx-tools.php
Authorization: Bearer {engine_api_token}
The legacy query form (?token={engine_api_token}) remains accepted for existing configurations but is deprecated — prefer the header.
Authorization: Bearer header. The legacy query form appears in URL-recording intermediaries' logs; if a URL containing a token may have been exposed, rotate the engine token. Cloudspire's own surfaces do not persist it: the request logger redacts credential parameters and the API server logs these routes without query strings.
Authentication
- Token (primary): the AI agent's response-engine API token, sent as
Authorization: Bearer {engine_api_token}. The legacy?token=query parameter is still read when no Bearer header is present (deprecated). The token is provisioned by your Cloudspire administrator on the agent's response engine. - Body
agent_id: one engine (and in some configurations one token) can back several agents. Theagent_idin the request body selects the exact agent — which fixes the tenant every tool operates on. Always send it; a token that is shared across agents is rejected (401) without it, and anagent_idthat does not belong to the token's engine is rejected (401/403). - Source IP: checked against the provider egress allowlist. Off-allowlist calls are logged for audit but not blocked — the token is the authentication.
Invocation Envelope
{
"name": "check_person_availability",
"args": { "extension": "201" },
"call": {
"call_id": "call_abc123",
"agent_id": "agent_xyz789"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | The tool to invoke (catalog below). Unknown names return 400. |
args | object | No | Tool arguments (per-tool tables below). Defaults to {}. |
call.call_id | string | No | The provider session ID, used to correlate the tool call with the call's history and logs. A top-level call_id is also accepted. |
call.agent_id | string | Recommended | The provider agent ID (see Authentication). A top-level agent_id is also accepted. |
Response
Every response — success or error — is a single JSON object with one key:
{ "result": "Sarah Johnson is available. RECOMMENDATION: transfer the call." }
The result string is written to be spoken by the AI. Error conditions use the same shape with an HTTP error status: 401 {"result": "Authentication error."}, 400 for a missing/unknown tool or malformed body, 500 for a handler failure, 503 when the platform database is unavailable.
Tool Catalog
Seven PBX tools are available to every AI agent. Arguments marked required must be present for a useful result.
| Tool | Purpose | Arguments |
|---|---|---|
get_company_directory |
Search the company directory for a person by name (fuzzy/phonetic matching is handled platform-side). | search_name (string, required) — the name exactly as the caller said it. |
get_business_status |
Business status: open/closed right now, hours, department extensions, and the emergency number. | none |
get_departments |
List available departments (queues and hunt groups) with descriptions. | none |
check_person_availability |
Presence check before a transfer: available, in a meeting, away, DND, or on another call, plus any custom away-message and an explicit RECOMMENDATION line (transfer / offer voicemail / ask the caller). | extension (string, required) — the extension number to check. |
lookup_caller |
Identify the caller by phone number in the company phonebook. | phone_number (string, required) — E.164. |
get_queue_status |
How many agents are available in a call center queue before routing a caller there. | extension (string, required) — the queue extension number. |
submit_message |
Submit a message or request: a message for a person, a callback request, an after-hours support/sales inquiry, or an emergency escalation. Generates the tenant's configured notification (email routing honors department-specific addresses). |
request_type (string, required) — message | callback | after_hours_support | after_hours_sales | emergencycaller_name (string, required)caller_phone (string, required)issue_summary (string, required)caller_company, caller_email (string, optional)for_person (string, optional) — who the message is for (with request_type message)reason (string, optional) — why the message is being takenurgency (string, optional) — normal | urgent | emergencydepartment (string, optional) — routes the notification to that department's configured email
|
Deep-CRM Tools
Three additional tools operate on the tenant's connected CRM (HubSpot, Salesforce, or Zoho). They are entitlement-gated: the tool must be enabled on the agent's engine configuration and the customer's plan must include the advanced CRM capability. A gated tool that is not enabled/entitled returns 200 with {"result": "CRM tools are not enabled for this agent."}.
| Tool | Purpose | Arguments |
|---|---|---|
crm_lookup_customer |
Look the caller up in the CRM; returns a record summary plus a crm_contact_id for follow-up actions. |
search (string, required) — E.164 phone number, email address, or full name. |
crm_add_note |
Save a note to the customer's CRM record. | note (string, required)crm_contact_id (string, preferred) — from a prior lookupsearch (string, fallback) — phone/email/name to locate the contact |
crm_create_contact |
Create a new CRM contact for a caller with no record yet (new lead intake). | last_name, phone (string, required)first_name, email, company (string, optional) |
Example
curl -X POST "https://api.cloudspirevoice.com/ai/pbx-tools.php" \
-H "Authorization: Bearer {engine_api_token}" \
-H "Content-Type: application/json" \
-d '{
"name": "check_person_availability",
"args": { "extension": "201" },
"call": { "call_id": "call_abc123", "agent_id": "agent_xyz789" }
}'
HTTP/1.1 200 OK
{ "result": "Sarah Johnson is in a meeting. RECOMMENDATION: offer to take a message." }
Internal Assistant Tools
The HTTP tool door for the two internal back-office tools used by Cloudspire's own website and admin-portal assistants: filing a GitHub issue (bug report or feature request) and creating a CRM support ticket. It exists so an external provider running one of those platform system assistants can invoke the tools over HTTP; it is not available to customer agents — only agents flagged platform-side as system agents pass authentication.
Endpoint
POST https://api.cloudspirevoice.com/external-ai/admin-tools.php
Authentication
- The response-engine API token, sent as an
Authorization: Bearer {token}header (preferred) or as a legacy?token=query parameter. - The body
agent_idselects the exact chat agent when a token is shared across agents. - The resolved agent must be an active, provider-run, platform system agent (the website/admin assistants). Any other combination — including a valid customer engine token — is rejected with
401.
Request Body
Tool arguments arrive wrapped in an args object (the provider's custom-tool convention); a flat body with the same fields at the top level is also accepted. The action field selects the tool.
{
"agent_id": "agent_xyz789",
"args": {
"action": "create_support_ticket",
"subject": "Portal login issue",
"description": "Customer reports the portal rejects their password after reset.",
"priority": "high",
"contact_name": "John Doe",
"contact_email": "[email protected]"
}
}
Action: create_github_issue
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | bug or feature. Anything else is rejected. |
title | string | Yes | Issue title. |
description | string | Yes | Issue body. A submitted-via-AI footer is appended. |
category | string | No | pbx | call-center | provisioning | messaging | reporting | docs | mobile; anything else defaults to pbx. Applied as an issue label. |
priority | string | No | low | medium | high | critical; defaults to medium. Applied as an issue label. |
Success response:
{
"success": true,
"issue_number": 1042,
"issue_url": "https://github.com/.../issues/1042",
"message": "Created bug report #1042. The team will review it shortly."
}
The support team is additionally notified by email. Failures return {"success": false, "error": "..."}.
Action: create_support_ticket
| Field | Type | Required | Description |
|---|---|---|---|
subject | string | Yes | Ticket subject. |
description | string | Yes | Ticket description (becomes the ticket's visible opening reply). |
category | string | No | A CRM ticket category name; unknown names fall back to Support. |
priority | string | No | low | medium | high | urgent; defaults to medium. |
contact_name | string | No | The requester's name, recorded on the ticket. |
contact_email | string | No | Matched against customer records: a match attributes the ticket to that customer account; an unmatched address is kept as a CC so replies still reach the requester. |
Success response:
{
"success": true,
"ticket_number": "TKT-10231",
"ticket_id": 512,
"message": "Support ticket TKT-10231 has been created. The support team will review it shortly."
}
An unknown action returns 400 {"error": "Unknown action. Use create_github_issue or create_support_ticket"}. Other errors: 401 missing/invalid credential, 405 non-POST, 400 invalid JSON, 500 database unavailable.
Example
curl -X POST "https://api.cloudspirevoice.com/external-ai/admin-tools.php" \
-H "Authorization: Bearer {engine_api_token}" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "agent_xyz789",
"args": {
"action": "create_github_issue",
"type": "bug",
"title": "Queue wallboard shows stale agent counts",
"description": "Reported via the admin assistant: the wallboard lags by several minutes.",
"category": "call-center",
"priority": "high"
}
}'
AI Outbound Calling
The outbound half of the surface — POST /agent/campaign-intake.php, which queues outbound AI calls from your own system — has its own full reference, including authentication (per-campaign Bearer key), the request/response schemas, queue statuses, and the completion callback: see the AI Outbound Calling API.
Not Part of This Surface
One further endpoint exists alongside these hooks but is not a customer-facing integration point: POST /ai/demo-intake.php?token={engine_api_token} accepts mid-call tool submissions (reservations, appointment requests, intake forms, and similar) only from agents flagged as demo agents — the assistants used for Cloudspire's own marketing demos. Any non-demo agent is rejected with 403, so it is listed here for completeness only.