API Overview
The Voice360 API v3 provides programmatic access to your Voice360 account for managing calls, queues, SMS, and user data.
Authentication
The Voice360 API uses API Key authentication for all requests. Pass your API key in the request headers for secure access.
How to Authenticate
API Key Authentication
Voice360 supports two types of API keys. Both use the same X-API-Key header:
User API Key
Acts as one specific user. Issued from that user's own settings.
- Mobile apps
- Browser extensions
- Personal integrations
Your own profile and numbers, plus the account-wide queue and availability endpoints. A user key is not a per-user sandbox. See the comparison below for exactly what it reaches.
abc123xyz...
Developer API Key
For account-level access to all data.
- Backend integrations
- Dashboards
- CRM integrations
All users, queues, and account-wide data
v360_dev_...
What Each Key Type Reaches
Both key types are bound to one account, and a request whose {account_id} does not match the key's account is rejected with 403. Within that account, access is decided per endpoint rather than by a single blanket rule:
| Endpoint | User Key | Developer Key |
|---|---|---|
/user/profile, /user/numbers |
Yes, always the key's own user | No. A developer key names no user, so these answer 401 |
/availability, /queues, /availability/queues |
Yes, account-wide. These return every user and queue on the account, not just the key's own user | Yes, account-wide |
/numbers |
No, 403 | Yes |
/webhooks and below |
No, 403 | Yes |
/sms/send |
Yes, but from_number must be a number assigned to that user |
Yes, from any number on the account |
/call/outbound |
Yes, but only as its own user, and the key must carry a scope satisfying voice:write |
Yes, naming any extension on the account |
/v3/ai/* |
No. The AI surface accepts developer keys only | Yes |
If your integration only ever acts for one person, a user key is still the right choice: it cannot manage webhooks, list the account's numbers, or send SMS from a number that person does not hold. Just do not treat it as a read barrier around queue and availability data, because it is not one.
Developer API Key Scopes
Developer keys can be restricted to specific scopes when created. Scopes are hierarchical: all grants everything, a bare API scope (e.g. ai) grants full read+write for that API, and :write implies :read. An endpoint that requires a scope shows it as a badge. Today that is the AI endpoints, webhook management and /call/outbound; the other voice endpoints declare no scope, so any developer key on the account reaches them.
| Scope | Grants |
|---|---|
all |
Every API, full access |
voice / ai |
Full read + write on that API |
voice:write / ai:write |
Read and write on that API (write implies read) |
voice:read / ai:read |
Read-only on that API — safe for inspection/QA tooling |
X-API-Key: your_api_key_here
💡 Pro Tip: Each endpoint in this documentation shows which key types it supports via colored badges. Look for User API Key or Developer API Key badges on each endpoint.
Every v3 Endpoint Requires a Key
There are no unauthenticated v3 endpoints. A request with no X-API-Key header is rejected before it reaches any handler:
{
"result": null,
"errors": true,
"error": "Invalid authentication method",
"message": "Authentication method public not allowed"
}
If you are building a public status page or wallboard, put your own service in front of the API and let it hold the key. Do not ship a Voice360 key to a browser.
Base URL & Versioning
Production Environment
https://api.voice360.app/v3/voice
Response Format
All API responses follow a consistent JSON structure for predictable integration:
{
"result": {
// Response data goes here
},
"errors": false,
"error": null,
"message": "Success"
}
{
"result": null,
"errors": true,
"error": "API key not found or invalid",
"message": "Invalid API key"
}
Response Fields
errors, not error. A client that reads response.error as a boolean sees a truthy string on failure and null on success, which happens to work, and then breaks the day an endpoint returns an error object. Branch on errors, or on the HTTP status code.
The envelope carries no top-level timestamp and no top-level error_code. Some endpoints include a timestamp inside result, and it is shown in that endpoint’s example when they do. Responses from the AI surface (/v3/ai/*) carry one extra envelope field, flag_for_review, which is internal and safe to ignore.
User Management Endpoints
Get real-time availability status for all users in an account. Shows online/offline state and active calls.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| account_id | integer | Required | Your Voice360 account ID |
Response Fields
to, from, call_seconds (elapsed seconds, fractional) and receiving (true when the user is the called party). Empty array when the user is not on a call.
Example Response
{
"result": [
{
"pk": 123,
"presence_id": "1001",
"callerID": "John Smith",
"first_name": "John",
"last_name": "Smith",
"devices_registered": true,
"channels": [
{
"to": "5551234",
"from": "1001",
"call_seconds": 45.31,
"receiving": false
}
]
}
],
"errors": false,
"error": null,
"message": "User availability data"
}
Queue Operations
List all queues in your account. Internal system queues are automatically filtered out.
Response Example
{
"result": [
{
"pk": 100134,
"name": "100134",
"queue_name": "Sales Queue",
"extension": "8001",
"strategy": "ringall",
"musiconhold": "default",
"timeout": 20,
"retry": 5,
"wrapuptime": 10,
"maxlen": 0,
"joinempty": "yes",
"leavewhenempty": "no",
"ringinuse": "no",
"assigned_users": []
},
{
"pk": 100135,
"name": "100135",
"queue_name": "Support Queue",
"extension": "8002",
"strategy": "fewestcalls",
"musiconhold": "default",
"timeout": 30,
"retry": 5,
"wrapuptime": 10,
"maxlen": 0,
"joinempty": "yes",
"leavewhenempty": "no",
"ringinuse": "no",
"assigned_users": []
}
],
"errors": false,
"error": null,
"message": "List of queues"
}
assigned_users is always an empty array on this endpoint. Queue membership is only loaded by GET /queues/{queue_id}, so fetch the queue individually when you need its members.
Get detailed information about a specific queue including configuration and assigned users.
Response Fields
user_id is the Voice360 user, membername is that user's extension, and interface is the Asterisk channel the queue rings. pk and uniqueid are the same value. state_interface is usually empty. Empty array when the queue has no members, or when membership could not be loaded.
Response Example
{
"result": {
"pk": 100134,
"name": "100134",
"queue_name": "Sales Queue",
"extension": "8001",
"strategy": "ringall",
"musiconhold": "default",
"timeout": 20,
"retry": 5,
"wrapuptime": 10,
"maxlen": 0,
"joinempty": "yes",
"leavewhenempty": "no",
"ringinuse": "no",
"assigned_users": [
{
"pk": 8812,
"uniqueid": 8812,
"user_id": 4412,
"membername": "1001",
"interface": "Local/1001@from-queue",
"state_interface": "",
"queue_id": 100134,
"queue_name": "100134",
"penalty": 0,
"wrapuptime": 1,
"ringinuse": "0",
"paused": 0
}
]
},
"errors": false,
"error": null,
"message": "Queue details"
}
Queue Availability
Get real-time agent availability metrics for all queues. Shows online agents and call activity.
Response Example
{
"result": [
{
"pk": 100134,
"name": "100134",
"queue_name": "Sales Queue",
"extension": "8001",
"is_ready": true,
"agents_online": 3,
"agents_available": 2,
"agents_on_call": 1,
"active_calls": 5
}
],
"errors": false,
"error": null,
"message": "Queue availability data"
}
Get detailed availability for a specific queue.
Response Fields
Call Management
Place an outbound call on behalf of a user. Voice360 rings that user first, on their registered devices or optionally on their cell, and dials the destination when they answer. This is click-to-call: the API starts the call and returns immediately, and there is no audio stream to attach to.
POST /v3/voice/{account_id}/calls is an alias for the same handler, with identical behaviour.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| from | string | Required | Extension number of the user the call is placed as. A developer key is account-level and names no user, so the call has to say whose call it is. Synonym: extension. Send user_id instead if you hold the Voice360 user ID rather than the extension; sending both is a 400. |
| to | string | Required | Destination number, 10 to 15 digits. E.164 (+15551234567) and plain NANP (5551234567) are both accepted, as are spaces, dashes, dots and parentheses. Internal extensions and feature codes are rejected. Synonym: destination. |
| user_id | integer | Voice360 user ID, as an alternative to from. Required instead of from when an extension is shared by more than one user on the account, which answers 409 rather than guessing. |
|
| caller_id | string | Asserts the number you expect to be presented. It does not choose one. Read Caller ID before sending it. | |
| recording | boolean | Asserts whether you expect the call to be recorded. It does not switch recording on or off. Read Recording before sending it. | |
| ring | string | "device" (default) rings the user's registered SIP devices. "cell" rings an external number instead. See Ringing a Cell First. |
|
| ring_number | string | The external number to ring when ring is "cell". Falls back to the user's call-forward number if omitted, and is a 400 if there is no fallback. Must not be the same number as to. |
|
| confirm | boolean | Only read when ring is "cell". Defaults to true, meaning the person who answers the cell must press 1 before the destination is dialled. See the caveat below. |
Caller ID
Per-call caller ID selection is not supported. The number presented comes from the user's outbound caller ID settings, which the dialplan applies to every outbound call. Nothing this endpoint sends can override it.
caller_id is therefore treated as an assertion about what you expect, and the endpoint refuses rather than quietly presenting something else:
- A number the account does not own is 403. Voice360 signs outbound calls with STIR/SHAKEN attestation A under its own carrier certificate. Presenting a number the account does not hold would put a falsely attested call on the PSTN, so it is refused outright rather than downgraded.
- A number the account owns but that is not the one that will be presented is 409, and the message names the number that will be presented instead. Send that number, change the user's outbound caller ID in the portal, or omit the field.
- The matching number is accepted and echoed back in the response.
Matching ignores formatting, so +13055550100 and 3055550100 are the same number.
Recording
Recording cannot be set per call. Whether a call is recorded is decided by the user's external call recording setting. The dialplan reads that setting and nothing else, so this endpoint cannot turn recording on or off for one call.
- Omit
recordingand the call is placed; the response reports whether it will be recorded. - Send a value that disagrees with the user's setting and the request is 409, naming the current setting and where to change it.
- Send a matching value and it is accepted.
The refusal is deliberate. Not recording a call your integration believes is recorded is a consent problem in two-party consent states, and recording one it believes is not is worse. The recording field in the response is always the truth about the call that was placed, whether or not you sent the field.
One wrinkle worth knowing: a user whose setting is "optional" is not recorded. Despite the name there is no per-call opt-in behind it today, so "optional" and "disabled" both mean recording: false.
Ringing a Cell First
By default the first leg goes to the user's registered SIP devices. Set "ring": "cell" with a ring_number to ring an external phone instead, which is how a rep takes a click-to-call while away from their desk.
ring: "cell" answers the rep before it dials your customer. With confirm at its default of true, the rep hears "To call this number, press 1" and the customer leg is placed only after they press it. Sending "confirm": false skips that gate, and you accept the consequence: if the cell does not answer and rolls to voicemail, the voicemail answers the leg and your customer is connected to the rep's voicemail greeting.
Example Request
curl -X POST https://api.voice360.app/v3/voice/61/call/outbound \
-H "X-API-Key: v360_dev_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"from": "1001",
"to": "+15551234567"
}'
Example Response
{
"result": {
"call_id": "c2c-1789432101",
"status": "initiating",
"from": "1001",
"to": "+15551234567",
"user_id": 4412,
"caller_id": "3055550100",
"recording": false,
"ring": "device",
"timestamp": "2026-09-16T17:31:18Z"
},
"errors": false,
"error": null,
"message": "Call initiated successfully"
}
Response Fields
c2c-<epoch seconds>. It is the platform's handle on the origination, not a CDR ID.
"initiating". The response returns before the first leg rings, so a 200 means the call was accepted and started, not that anyone answered.
"device" or "cell", echoing what was used.
Errors
| Status | When |
|---|---|
| 400 | Missing from or to, both from and user_id sent, a destination that is not a valid external number, a non-boolean recording or confirm, an unknown ring value, or ring: "cell" with no usable number. |
| 401 | Missing or unknown API key. |
| 403 | The key lacks voice:write, the URL account does not match the key's account, caller_id is not a number on the account, or a user key named somebody other than its own user. |
| 404 | No user with that extension or ID on this account. A user that exists on a different account also answers 404, so the response never confirms an ID belongs to someone else. |
| 409 | The extension matches more than one user on the account (send user_id), or caller_id / recording disagree with what the platform will actually do. |
| 503 | ring: "cell" with confirm: true and no confirmation context deployed. |
SMS Services
Send an SMS message from your Voice360 phone number.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| from | string | Required | Your Voice360 phone number |
| to | string | Required | Recipient phone number |
| message | string | Required | SMS message content (max 160 chars) |
Example Request
const sendSMS = async () => {
const response = await fetch('https://api.voice360.app/v3/voice/61/sms/send', {
method: 'POST',
headers: {
'X-API-Key': 'your_api_key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
from: '+15559876543',
to: '+15551234567',
message: 'Hello from Voice360!'
})
});
const result = await response.json();
console.log(result);
};
Phone Numbers
List all phone numbers (DIDs) in the account. Returns every number regardless of user assignment. Use query parameters to filter results.
Query Parameters
Response Fields
Example Response
{
"result": [
{
"pk": 789,
"number": "+15551234567",
"friendly_name": "Main Business Line",
"sms_enabled": true,
"mms_enabled": true
},
{
"pk": 790,
"number": "+15559876543",
"friendly_name": "Support Line",
"sms_enabled": true,
"mms_enabled": false
},
{
"pk": 791,
"number": "+15551112222",
"friendly_name": "AI Agent Line",
"sms_enabled": false,
"mms_enabled": false
}
],
"errors": false,
"error": null,
"message": "List of numbers"
}
Example Request
const getAccountNumbers = async () => {
const response = await fetch('https://api.voice360.app/v3/voice/61/numbers', {
method: 'GET',
headers: {
'X-API-Key': 'v360_dev_your_developer_key'
}
});
const result = await response.json();
console.log('Account numbers:', result.result);
};
// With filters
const getActiveNumbers = async () => {
const response = await fetch('https://api.voice360.app/v3/voice/61/numbers?status=active', {
method: 'GET',
headers: {
'X-API-Key': 'v360_dev_your_developer_key'
}
});
const result = await response.json();
console.log('Active numbers:', result.result);
};
Get list of phone numbers (DIDs) assigned to the authenticated user for making calls and sending SMS.
Response Fields
Example Response
{
"result": [
{
"pk": 789,
"number": "+15551234567",
"friendly_name": "Main Business Line",
"sms_enabled": true,
"mms_enabled": true
},
{
"pk": 790,
"number": "+15559876543",
"friendly_name": "Support Line",
"sms_enabled": true,
"mms_enabled": false
}
],
"errors": false,
"error": null,
"message": "List of phone numbers assigned to user"
}
Example Request
const getUserNumbers = async () => {
const response = await fetch('https://api.voice360.app/v3/voice/61/user/numbers', {
method: 'GET',
headers: {
'X-API-Key': 'your_api_key'
}
});
const result = await response.json();
console.log('My phone numbers:', result.result);
};
User Profile
Get the authenticated user's profile and extension details including voicemail settings, call forwarding, and device information.
Response Fields
Example Response
{
"result": {
"pk": 123,
"asterisk_user_id": 456,
"presence_id": "1001",
"callerID": "John Smith",
"first_name": "John",
"last_name": "Smith",
"email": "john.smith@company.com",
"voicemail_enabled": true,
"forward_enabled": false,
"forward_number": null,
"time_zone": "America/New_York"
},
"errors": false,
"error": null,
"message": "User profile details"
}
AI Agents
Manage Voice360 AI voice agents programmatically — the same configuration surface as the AI Agents tab in the portal, available via Developer API Key. Every request is account-scoped: the {account_id} in the URL must match the key's account, and agent IDs are verified to belong to that account before any read or write.
modern_config.agent_config.custom_functions via PATCH /agents/{agent_id} — there is no separate tools endpoint. Changes take effect on the agent's next call/session, no redeploy needed.
List AI agents on the account.
Query Parameters
Example Request
const listAgents = async () => {
const response = await fetch('https://api.voice360.app/v3/ai/61/agents', {
method: 'GET',
headers: { 'X-API-Key': 'v360_dev_your_developer_key' }
});
const result = await response.json();
console.log('Agents:', result.result);
};
Create a new AI agent. account_id is always taken from the URL, not the request body.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Required | Agent display name |
| bot_type | string | Optional | Agent type (call, sms, etc.) |
| modern_config | object | Optional | Model, STT/TTS/voice, capabilities, and custom_functions (see note above) |
Get an agent's full configuration, including modern_config (model, STT/TTS/voice, capabilities, custom functions).
Partially update an agent. Send only the fields you're changing — this is also how you add or edit custom function tools.
Example Request — Add a Booking Tool
const addBookingTool = async () => {
const agentRes = await fetch('https://api.voice360.app/v3/ai/61/agents/45', {
headers: { 'X-API-Key': 'v360_dev_your_developer_key' }
});
const agent = (await agentRes.json()).result;
agent.modern_config.agent_config.custom_functions.push({
id: 'webhook-book-appt',
name: 'book_appointment',
description: 'Book a consultation after collecting name, phone and preferred time',
url: 'https://your-backend.example.com/api/book',
method: 'POST',
headers: [{ key: 'Authorization', value: 'Bearer <token>' }],
parameters: [
{ name: 'name', type: 'string', description: 'Client full name', required: true },
{ name: 'phone', type: 'string', description: 'Callback number', required: true },
{ name: 'slot', type: 'string', description: 'Chosen time slot', required: true }
],
enabled: true
});
const response = await fetch('https://api.voice360.app/v3/ai/61/agents/45', {
method: 'PATCH',
headers: {
'X-API-Key': 'v360_dev_your_developer_key',
'Content-Type': 'application/json'
},
body: JSON.stringify({ modern_config: agent.modern_config })
});
console.log(await response.json());
};
Delete an agent.
Clone an agent (new agent named "<original> (Copy)").
Rebuild call_prompt / message_prompt from modern_config.
AI Knowledge Base
Each AI agent has its own knowledge base of documents and text notes, used for retrieval-augmented answers during calls. Uploaded documents are chunked and embedded asynchronously — poll the document list until processing_complete is true.
List documents, including processing/embedding status.
Upload a document (multipart form, field name file).
Get a single document's metadata and processing status.
Delete a document.
List text notes. Filters: category, enabled_only.
Create a text note.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| title | string | Required | Note title |
| content | string | Required | Note body, used for retrieval |
| category | string | Optional | Grouping category |
| tags | array | Optional | List of string tags |
Update a text note (partial).
Delete a text note.
AI Calls, Transcripts & Grading
Read-only inspection of AI call activity — useful for QA and debugging agent behavior without opening the portal. All endpoints require the ai:read scope (also satisfied by ai / all).
List AI call records. Filters: ai_agent_id, call_direction, resource_type.
Get a single AI call record.
List call timelines. The events field contains the full conversation timeline and tool-execution traces — what custom functions ran, with what params, and what came back. Filters: aicall_uuid, ai_agent_id (single or comma-separated), resource_type, started_at_from/started_at_to (ISO-8601).
Get a single call timeline.
List AI call grades. Filters: resource_type, grade, start_date, end_date.
Get the grade for a single call.
Webhooks
Webhooks allow you to receive real-time notifications when events occur in your Voice360 account. Configure a webhook URL to receive HTTP POST requests for events like incoming calls, SMS messages, and queue activity.
Managing Webhook Subscriptions
Webhook subscriptions can be managed in the portal, or programmatically with a Developer API Key:
| Method | Endpoint | Scope | Description |
|---|---|---|---|
| GET | /v3/voice/{account_id}/webhooks |
voice:read | List webhook subscriptions |
| POST | /v3/voice/{account_id}/webhooks |
voice:write | Create a webhook subscription |
| GET | /v3/voice/{account_id}/webhooks/{webhook_id} |
voice:read | Get a single webhook subscription |
| PATCH | /v3/voice/{account_id}/webhooks/{webhook_id} |
voice:write | Update a webhook subscription |
| DELETE | /v3/voice/{account_id}/webhooks/{webhook_id} |
voice:write | Delete a webhook subscription |
| POST | /v3/voice/{account_id}/webhooks/{webhook_id}/rotate-secret |
voice:write | Rotate the HMAC signing secret (returned once) |
How Webhooks Work
- Configure a webhook in your Voice360 portal with your endpoint URL and event type
- When the event occurs, Voice360 sends an HTTP POST request to your URL with event data
- Your server processes the webhook payload and responds with 200 OK
- If delivery fails, Voice360 automatically retries based on your retry configuration
Payload Envelope
Every webhook payload uses the same top-level envelope. The metadata object contains event-specific fields documented below.
{
"event_name": "EVENT_NAME",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": { /* event-specific fields */ }
}
Available Event Types
Call Events
| INCOMING_EXTERNAL_CALL | Fires when an external call comes in, before it's answered |
| CALL_ENDED | Fires when any call (inbound or outbound) ends |
| USER_DIAL | Fires when a user initiates an outbound call |
| USER_ANSWER | Fires when a user answers an incoming call |
| USER_HANGUP | Fires when an outbound call ends (user hangs up or call is not answered) |
SMS Events
| SMS_RECEIVED | Fires when an SMS message is received from a contact |
| SMS_SENT | Fires when an SMS message is sent to a contact |
| SMS_DELIVERED | Fires when an SMS delivery confirmation is received from the carrier |
Queue Events
| QUEUE_ENTER | Fires when a caller enters a queue |
| QUEUE_MEMBER_DIAL | Fires when a queue agent is being dialed |
| QUEUE_MEMBER_ANSWER | Fires when a queue agent answers |
| QUEUE_MEMBER_DIAL_HANGUP | Fires when a queue agent's call ends |
| QUEUE_HANGUP | Fires when a caller leaves a queue |
| QUEUE_NO_ANSWER | Call not answered by any agent |
| QUEUE_HANGUP_TIMEOUT | Caller times out waiting in queue |
| QUEUE_HANGUP_ABANDONED | Caller hangs up while waiting |
Contact Events
| CONTACT_CREATED | Fires when a new contact is created |
| CONTACT_UPDATED | Fires when a contact is updated |
| CONTACT_DELETED | Fires when a contact is deleted |
AI & Transcription Events
| TRANSCRIPTION_FINISHED | Fires when a call transcription completes. Includes the full AWS Call Analytics–style transcription JSON with per-channel transcripts, sentiment, talk-time, and an AI-generated CallRecap. |
Example Payloads
Each event below shows the JSON body Voice360 will POST to your webhook URL. All payloads share the same envelope (event_name, account_id, timestamp, metadata) — only the metadata fields differ by event type.
INCOMING_EXTERNAL_CALL
{
"event_name": "INCOMING_EXTERNAL_CALL",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": {
"caller_id_number": "+15551234567",
"caller_id_name": "John Doe",
"dnis": "+15559876543",
"uniqueid": "1704067200.12345"
}
}
CALL_ENDED
{
"event_name": "CALL_ENDED",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": {
"caller_id_number": "+15551234567",
"dnis": "+15559876543",
"uniqueid": "1704067200.12345",
"call_direction": "inbound",
"to_user_id": "67890",
"duration": 120
}
}
USER_DIAL
{
"event_name": "USER_DIAL",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": {
"dialed_number": "+15551234567",
"from_user_id": "67890",
"uniqueid": "1704067200.12345"
}
}
USER_ANSWER
{
"event_name": "USER_ANSWER",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": {
"caller_id_number": "+15551234567",
"to_user_id": "67890",
"uniqueid": "1704067200.12345"
}
}
USER_HANGUP
{
"event_name": "USER_HANGUP",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": {
"dialed_number": "+15551234567",
"from_user_id": "67890",
"uniqueid": "1704067200.12345",
"duration": 45
}
}
SMS_RECEIVED
{
"event_name": "SMS_RECEIVED",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": {
"from_number": "+15551234567",
"to_number": "+15559876543",
"body": "Hello, this is a test message",
"message_id": "msg_abc123"
}
}
SMS_SENT
{
"event_name": "SMS_SENT",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": {
"from_number": "+15559876543",
"to_number": ["+15551234567"],
"body": "Hello from Voice360!",
"message_id": "msg_xyz789",
"sms_sent_by_user_id": "67890"
}
}
SMS_DELIVERED
{
"event_name": "SMS_DELIVERED",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": {
"to_number": ["+15551234567"],
"message_id": "msg_xyz789",
"status": "delivered"
}
}
QUEUE_ENTER
{
"event_name": "QUEUE_ENTER",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": {
"queue_id": "queue_123",
"queuecall_id": "456",
"uniqueid": "1704067200.12345",
"queuename": "queue_123",
"callerid_number": "+15551234567",
"callerid_name": "John Doe"
}
}
QUEUE_MEMBER_DIAL
{
"event_name": "QUEUE_MEMBER_DIAL",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": {
"App-Name": "Queue",
"to_user_id": "67890",
"from_user_id": null,
"uniqueid": "1704067200.12345",
"caller_id_number": "+15551234567",
"dnis": "+15559876543",
"queuecall_id": "456"
}
}
QUEUE_MEMBER_ANSWER
{
"event_name": "QUEUE_MEMBER_ANSWER",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": {
"App-Name": "Queue",
"to_user_id": "67890",
"caller_id_number": "+15551234567",
"uniqueid": "1704067200.12345",
"dnis": "+15559876543",
"queuecall_id": "456"
}
}
QUEUE_MEMBER_DIAL_HANGUP
{
"event_name": "QUEUE_MEMBER_DIAL_HANGUP",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": {
"App-Name": "Queue",
"to_user_id": "67890",
"from_user_id": null,
"uniqueid": "1704067200.12345",
"caller_id_number": "+15551234567",
"dnis": "+15559876543",
"queuecall_id": "456",
"duration": 120
}
}
QUEUE_HANGUP
{
"event_name": "QUEUE_HANGUP",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": {
"App-Name": "Queue",
"queue_id": "queue_123",
"queuecall_id": "456",
"uniqueid": "1704067200.12345",
"queuename": "queue_123",
"duration": 16.59,
"dnis": "+15559876543",
"dialed_number": "+15559876543",
"caller_id_number": "+15551234567"
}
}
QUEUE_NO_ANSWER
{
"event_name": "QUEUE_NO_ANSWER",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": {
"caller_id_number": "+15551234567",
"queue_name": "Sales Queue",
"queue_id": "queue_123",
"wait_time": 60
}
}
QUEUE_HANGUP_TIMEOUT
{
"event_name": "QUEUE_HANGUP_TIMEOUT",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": {
"caller_id_number": "+15551234567",
"queue_name": "Support Queue",
"queue_id": "queue_456",
"wait_time": 300
}
}
QUEUE_HANGUP_ABANDONED
{
"event_name": "QUEUE_HANGUP_ABANDONED",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": {
"caller_id_number": "+15551234567",
"queue_name": "Support Queue",
"queue_id": "queue_456",
"wait_time": 45
}
}
CONTACT_CREATED
{
"event_name": "CONTACT_CREATED",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": {
"contact_id": "98765",
"phone": "+15551234567",
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com"
}
}
CONTACT_UPDATED
{
"event_name": "CONTACT_UPDATED",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": {
"contact_id": "98765",
"updated_fields": ["first_name", "email"],
"phone": "+15551234567"
}
}
CONTACT_DELETED
{
"event_name": "CONTACT_DELETED",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": {
"contact_id": "98765",
"phone": "+15551234567"
}
}
TRANSCRIPTION_FINISHED
Fires when a call transcription completes. The metadata.transcription field is a JSON-encoded string containing AWS Call Analytics–style data. Parse with JSON.parse() on your end before consuming.
{
"event_name": "TRANSCRIPTION_FINISHED",
"account_id": "12345",
"timestamp": 1704067200,
"metadata": {
"uniqueid": "1704067200.12345",
"transcription": "{\"AccountId\":\"12345\", ...}",
"provider": "together_ai",
"model": "openai/whisper-large-v3",
"total_tokens": 1842,
"success": true
}
}
After parsing metadata.transcription, the resulting object follows this shape:
{
"AccountId": "12345",
"JobName": "call-analytics-1704067200.12345",
"LanguageCode": "en-US",
"JobStatus": "COMPLETED",
"Channel": "VOICE",
"Participants": [
{ "ParticipantRole": "AGENT" },
{ "ParticipantRole": "CUSTOMER" }
],
"ConversationCharacteristics": {
"TotalConversationDurationMillis": 312000,
"TalkTime": { "TotalTimeMillis": 290000, "DetailsByParticipant": { ... } },
"TalkSpeed": { "DetailsByParticipant": { ... } },
"Sentiment": {
"OverallSentiment": { "AGENT": 0.6, "CUSTOMER": 0.2 },
"SentimentByPeriod": { ... }
}
},
"ContactSummary": {
"AutoGenerated": {
"OverallSummary": { "Content": "Customer called about billing question..." }
}
},
"Transcript": [
{
"Id": "uuid",
"ParticipantRole": "AGENT",
"BeginOffsetMillis": 0,
"EndOffsetMillis": 4200,
"Content": "Thanks for calling, how can I help?",
"Sentiment": "NEUTRAL"
}
],
"CallRecap": {
"summary": "Customer asked about a billing discrepancy on their May invoice...",
"outcome": "resolved",
"action_items": ["Send updated invoice", "Refund $12.50 credit"],
"topics": ["billing", "invoice", "refund"]
},
"AgentTranscript": [
{ "start": 0, "end": 4.2, "text": "Thanks for calling, how can I help?", "confidence": 0.97 }
],
"CustomerTranscript": [
{ "start": 4.5, "end": 8.1, "text": "Hi, I have a question about my bill.", "confidence": 0.95 }
],
"CompletedAt": "2026-05-20T18:30:00.000000Z"
}
success: false, transcription: null, and a top-level metadata.error string describing the reason. Always check metadata.success before parsing metadata.transcription.
Configuring Webhooks
Webhooks are configured through the Voice360 portal at Settings → Webhooks. For each webhook, you'll configure:
- Name: Descriptive name for the webhook
- URL: Your endpoint URL (must be HTTPS in production)
- Event Type: Which event to listen for
- HTTP Method: GET or POST (POST recommended)
- Retries: Number of retry attempts (1-10)
On create, the portal displays a signing secret exactly once. Save it — it cannot be retrieved later. If you lose it, click the key icon in the webhooks list to rotate the secret and get a fresh value.
Verifying Webhook Signatures
Every POST webhook delivery includes an X-Voice360-Signature header containing an HMAC-SHA256 of the raw request body, keyed by your webhook's signing secret. Verifying the signature lets you confirm the request actually came from Voice360 and hasn't been tampered with in transit.
Verification is optional — if your endpoint is on a private URL or you're comfortable trusting any caller, you can ignore the header. We recommend verifying for any production integration.
Header format:
X-Voice360-Signature: sha256=<64-char-hex-hmac>
Verification (Node.js):
const crypto = require('crypto');
function verifyVoice360Signature(rawBody, signatureHeader, signingSecret) {
// signatureHeader format: "sha256=<hex>"
const [scheme, providedHex] = (signatureHeader || '').split('=');
if (scheme !== 'sha256' || !providedHex) return false;
const expectedHex = crypto
.createHmac('sha256', signingSecret)
.update(rawBody) // raw bytes of the request body
.digest('hex');
// Constant-time comparison
const a = Buffer.from(providedHex, 'hex');
const b = Buffer.from(expectedHex, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express example
app.post('/voice360-webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const ok = verifyVoice360Signature(
req.body, // Buffer of raw bytes
req.header('X-Voice360-Signature'),
process.env.VOICE360_WEBHOOK_SECRET
);
if (!ok) return res.status(401).send('Bad signature');
const event = JSON.parse(req.body.toString('utf8'));
// ... handle event
res.sendStatus(200);
});
Verification (Python):
import hmac
import hashlib
def verify_voice360_signature(raw_body: bytes, signature_header: str, signing_secret: str) -> bool:
"""signature_header format: 'sha256=<hex>'"""
if not signature_header or not signature_header.startswith('sha256='):
return False
provided_hex = signature_header.split('=', 1)[1]
expected_hex = hmac.new(
signing_secret.encode('utf-8'),
raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(provided_hex, expected_hex)
# Flask example
from flask import request, abort
@app.route('/voice360-webhook', methods=['POST'])
def voice360_webhook():
raw = request.get_data() # raw bytes, NOT request.json
if not verify_voice360_signature(
raw,
request.headers.get('X-Voice360-Signature'),
os.environ['VOICE360_WEBHOOK_SECRET'],
):
abort(401)
event = json.loads(raw)
# ... handle event
return '', 200
request.get_data() (Flask), req.rawBody / express.raw() (Express), or request.body read once before parsing.
Rotating Signing Secrets
From the Webhooks page, click the key icon next to a webhook to rotate its secret. The old secret stops being valid the moment you confirm, and the new secret is displayed once. Update your verification code with the new value before traffic resumes — otherwise signature checks will fail until you redeploy.
Best Practices
- Always respond with 200 OK quickly (process asynchronously if needed)
- Verify the
X-Voice360-Signatureheader for any production integration - Use HTTPS endpoints in production
- Implement idempotency — you may receive the same event multiple times
- Log all webhook requests for debugging
- Store the signing secret in a secret manager / env var, not in source control
Code Examples
Complete examples showing how to integrate with the Voice360 API in popular programming languages.
#!/bin/bash
# Set your API credentials
API_KEY="your_api_key"
ACCOUNT_ID="61"
BASE_URL="https://api.voice360.app/v3/voice"
# Get user availability
echo "Fetching user availability..."
curl -H "X-API-Key: $API_KEY" \
"$BASE_URL/$ACCOUNT_ID/availability"
# List all queues
echo "Fetching queues..."
curl -H "X-API-Key: $API_KEY" \
"$BASE_URL/$ACCOUNT_ID/queues"
# Get queue availability
echo "Fetching queue availability..."
curl -H "X-API-Key: $API_KEY" \
"$BASE_URL/$ACCOUNT_ID/availability/queues"
# Initiate an outbound call
echo "Initiating outbound call..."
curl -X POST "$BASE_URL/$ACCOUNT_ID/call/outbound" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "1001",
"to": "+15551234567",
"recording": true
}'
class Voice360API {
constructor(apiKey, accountId) {
this.apiKey = apiKey;
this.accountId = accountId;
this.baseUrl = 'https://api.voice360.app/v3/voice';
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}/${this.accountId}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
'X-API-Key': this.apiKey,
'Content-Type': 'application/json',
...options.headers
}
});
if (!response.ok) {
throw new Error(`API Error: ${response.status} ${response.statusText}`);
}
return response.json();
}
// User methods
async getUserAvailability() {
return this.request('/availability');
}
// Queue methods
async getQueues() {
return this.request('/queues');
}
async getQueueDetails(queueId) {
return this.request(`/queues/${queueId}`);
}
async getQueueAvailability(queueId = null) {
const endpoint = queueId
? `/availability/queues/${queueId}`
: '/availability/queues';
return this.request(endpoint);
}
// Call methods
async initiateCall(from, to, options = {}) {
return this.request('/call/outbound', {
method: 'POST',
body: JSON.stringify({ from, to, ...options })
});
}
// SMS methods
async sendSMS(from, to, message) {
return this.request('/sms/send', {
method: 'POST',
body: JSON.stringify({ from, to, message })
});
}
}
// Usage example
async function main() {
const api = new Voice360API('your_api_key', 61);
try {
// Get all online users
const availability = await api.getUserAvailability();
const onlineUsers = availability.result.filter(u => u.devices_registered);
console.log(`${onlineUsers.length} users online`);
// Check queue status
const queues = await api.getQueueAvailability();
const readyQueues = queues.result.filter(q => q.is_ready);
console.log(`${readyQueues.length} queues have agents available`);
// Make an outbound call
const call = await api.initiateCall('1001', '+15551234567', {
caller_id: '+15559876543',
recording: true
});
console.log(`Call initiated: ${call.result.call_id}`);
} catch (error) {
console.error('API Error:', error);
}
}
main();
import requests
import json
from typing import Optional, Dict, Any
class Voice360API:
"""Voice360 API Client for Python"""
def __init__(self, api_key: str, account_id: int):
self.api_key = api_key
self.account_id = account_id
self.base_url = 'https://api.voice360.app/v3/voice'
self.session = requests.Session()
self.session.headers.update({
'X-API-Key': api_key,
'Content-Type': 'application/json'
})
def _request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
"""Make an API request"""
url = f'{self.base_url}/{self.account_id}{endpoint}'
response = self.session.request(method, url, **kwargs)
response.raise_for_status()
return response.json()
def get_user_availability(self) -> Dict[str, Any]:
"""Get user availability status"""
return self._request('GET', '/availability')
def get_queues(self) -> Dict[str, Any]:
"""List all queues"""
return self._request('GET', '/queues')
def get_queue_details(self, queue_id: int) -> Dict[str, Any]:
"""Get details for a specific queue"""
return self._request('GET', f'/queues/{queue_id}')
def get_queue_availability(self, queue_id: Optional[int] = None) -> Dict[str, Any]:
"""Get queue availability metrics"""
endpoint = f'/availability/queues/{queue_id}' if queue_id else '/availability/queues'
return self._request('GET', endpoint)
def initiate_call(self, from_ext: str, to_number: str, **options) -> Dict[str, Any]:
"""Initiate an outbound call"""
data = {'from': from_ext, 'to': to_number, **options}
return self._request('POST', '/call/outbound', json=data)
def send_sms(self, from_number: str, to_number: str, message: str) -> Dict[str, Any]:
"""Send an SMS message"""
data = {'from': from_number, 'to': to_number, 'message': message}
return self._request('POST', '/sms/send', json=data)
def main():
# Initialize the API client
api = Voice360API('your_api_key', 61)
try:
# Get user availability
availability = api.get_user_availability()
online_users = [u for u in availability['result'] if u['devices_registered']]
print(f"Online users: {len(online_users)}")
# Check queue status
queues = api.get_queue_availability()
ready_queues = [q for q in queues['result'] if q['is_ready']]
print(f"Queues with agents: {len(ready_queues)}")
# Display queue details
for queue in ready_queues:
print(f" - {queue['queue_name']}: {queue['agents_online']} agents online")
# Initiate a call
call = api.initiate_call(
from_ext='1001',
to_number='+15551234567',
caller_id='+15559876543',
recording=True
)
print(f"Call initiated: {call['result']['call_id']}")
# Send an SMS
sms = api.send_sms(
from_number='+15559876543',
to_number='+15551234567',
message='Hello from Voice360!'
)
print(f"SMS sent: {sms['message']}")
except requests.exceptions.RequestException as e:
print(f"API Error: {e}")
except KeyError as e:
print(f"Unexpected response format: {e}")
if __name__ == '__main__':
main()
<?php
class Voice360API {
private $apiKey;
private $accountId;
private $baseUrl = 'https://api.voice360.app/v3/voice';
public function __construct($apiKey, $accountId) {
$this->apiKey = $apiKey;
$this->accountId = $accountId;
}
private function request($method, $endpoint, $data = null) {
$url = "{$this->baseUrl}/{$this->accountId}{$endpoint}";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-API-Key: ' . $this->apiKey,
'Content-Type: application/json'
]);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
if ($data) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
}
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode >= 400) {
throw new Exception("API Error: HTTP {$httpCode}");
}
return json_decode($response, true);
}
public function getUserAvailability() {
return $this->request('GET', '/availability');
}
public function getQueues() {
return $this->request('GET', '/queues');
}
public function getQueueDetails($queueId) {
return $this->request('GET', "/queues/{$queueId}");
}
public function getQueueAvailability($queueId = null) {
$endpoint = $queueId
? "/availability/queues/{$queueId}"
: '/availability/queues';
return $this->request('GET', $endpoint);
}
public function initiateCall($from, $to, $options = []) {
$data = array_merge(['from' => $from, 'to' => $to], $options);
return $this->request('POST', '/call/outbound', $data);
}
public function sendSMS($from, $to, $message) {
return $this->request('POST', '/sms/send', [
'from' => $from,
'to' => $to,
'message' => $message
]);
}
}
// Usage example
try {
$api = new Voice360API('your_api_key', 61);
// Get user availability
$availability = $api->getUserAvailability();
$onlineUsers = array_filter($availability['result'], function($u) {
return $u['devices_registered'];
});
echo "Online users: " . count($onlineUsers) . "\n";
// Check queue status
$queues = $api->getQueueAvailability();
foreach ($queues['result'] as $queue) {
if ($queue['is_ready']) {
echo "Queue {$queue['queue_name']}: {$queue['agents_online']} agents online\n";
}
}
// Make a call
$call = $api->initiateCall('1001', '+15551234567', [
'caller_id' => '+15559876543',
'recording' => true
]);
echo "Call initiated: {$call['result']['call_id']}\n";
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
?>
using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Text;
using Newtonsoft.Json;
public class Voice360API
{
private readonly HttpClient client;
private readonly string accountId;
private readonly string baseUrl = "https://api.voice360.app/v3/voice";
public Voice360API(string apiKey, int accountId)
{
this.accountId = accountId.ToString();
client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", apiKey);
}
private async Task<T> RequestAsync<T>(HttpMethod method, string endpoint, object data = null)
{
var url = $"{baseUrl}/{accountId}{endpoint}";
var request = new HttpRequestMessage(method, url);
if (data != null)
{
var json = JsonConvert.SerializeObject(data);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(responseJson);
}
public async Task<dynamic> GetUserAvailabilityAsync()
{
return await RequestAsync<dynamic>(HttpMethod.Get, "/availability");
}
public async Task<dynamic> GetQueuesAsync()
{
return await RequestAsync<dynamic>(HttpMethod.Get, "/queues");
}
public async Task<dynamic> InitiateCallAsync(string from, string to, object options = null)
{
var data = new { from, to };
if (options != null)
{
// Merge options with data
var json = JsonConvert.SerializeObject(data);
var optionsJson = JsonConvert.SerializeObject(options);
var merged = JsonConvert.DeserializeObject<dynamic>(json);
var optionsObj = JsonConvert.DeserializeObject<dynamic>(optionsJson);
foreach (var prop in optionsObj)
{
merged[prop.Name] = prop.Value;
}
data = merged;
}
return await RequestAsync<dynamic>(HttpMethod.Post, "/call/outbound", data);
}
}
// Usage
class Program
{
static async Task Main(string[] args)
{
var api = new Voice360API("your_api_key", 61);
try
{
// Get user availability
var availability = await api.GetUserAvailabilityAsync();
Console.WriteLine($"Users fetched: {availability.result.Count}");
// Get queues
var queues = await api.GetQueuesAsync();
Console.WriteLine($"Queues fetched: {queues.result.Count}");
// Initiate a call
var call = await api.InitiateCallAsync("1001", "+15551234567", new {
caller_id = "+15559876543",
recording = true
});
Console.WriteLine($"Call initiated: {call.result.call_id}");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
import java.util.HashMap;
public class Voice360API {
private final HttpClient client;
private final String apiKey;
private final int accountId;
private final String baseUrl = "https://api.voice360.app/v3/voice";
private final ObjectMapper mapper;
public Voice360API(String apiKey, int accountId) {
this.apiKey = apiKey;
this.accountId = accountId;
this.client = HttpClient.newHttpClient();
this.mapper = new ObjectMapper();
}
private Map<String, Object> request(String method, String endpoint, Map<String, Object> data) throws Exception {
String url = baseUrl + "/" + accountId + endpoint;
HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("X-API-Key", apiKey)
.header("Content-Type", "application/json");
if ("POST".equals(method) && data != null) {
String json = mapper.writeValueAsString(data);
builder.POST(HttpRequest.BodyPublishers.ofString(json));
} else {
builder.GET();
}
HttpRequest request = builder.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) {
throw new RuntimeException("API Error: " + response.statusCode());
}
return mapper.readValue(response.body(), Map.class);
}
public Map<String, Object> getUserAvailability() throws Exception {
return request("GET", "/availability", null);
}
public Map<String, Object> getQueues() throws Exception {
return request("GET", "/queues", null);
}
public Map<String, Object> initiateCall(String from, String to, Map<String, Object> options) throws Exception {
Map<String, Object> data = new HashMap<>();
data.put("from", from);
data.put("to", to);
if (options != null) {
data.putAll(options);
}
return request("POST", "/call/outbound", data);
}
public static void main(String[] args) {
Voice360API api = new Voice360API("your_api_key", 61);
try {
// Get user availability
Map<String, Object> availability = api.getUserAvailability();
System.out.println("Availability: " + availability);
// Get queues
Map<String, Object> queues = api.getQueues();
System.out.println("Queues: " + queues);
// Initiate call
Map<String, Object> options = new HashMap<>();
options.put("caller_id", "+15559876543");
options.put("recording", true);
Map<String, Object> call = api.initiateCall("1001", "+15551234567", options);
System.out.println("Call initiated: " + call);
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Rate Limits
X-RateLimit-Limit or X-RateLimit-Remaining header, and no v3 endpoint returns 429 because of request volume.
That describes today, and it is not a promise about tomorrow. Limits may be introduced, and when they are, they will appear here first and will use the standard X-RateLimit-* headers and a 429 status. Write your client so a 429 or a 5xx triggers an exponential backoff and a retry, even though neither is produced by volume right now.
Please still be reasonable. Poll /availability on an interval that matches how fast you can actually use the data rather than in a tight loop, and prefer webhooks over polling whenever an event exists for what you are watching.
Limits That Do Exist
Two things are metered and are sometimes mistaken for API rate limits:
- Outbound SMS pacing. Campaign and bulk SMS traffic is paced per account against carrier throughput. That pacing happens inside the platform after
POST /sms/sendhas already returned 200, so it affects when a message leaves, not whether your API call is accepted. - Carrier and 10DLC limits. A number's registered 10DLC campaign governs how much SMS traffic it may carry. That ceiling belongs to the carrier, not to this API, and exceeding it surfaces as delivery failures rather than as HTTP errors.
Error Handling
Errors use the same envelope as everything else. errors is true, result is null, message is meant for a human, and error carries the machine-readable part.
error_code field, and there are no BAD_REQUEST-style codes.
Branch on the HTTP status code, and read error for the detail.
The Two Shapes of error
Most endpoints put a short string in error:
{
"result": null,
"errors": true,
"error": "API key not found or invalid",
"message": "Invalid API key"
}
Endpoints that surface a platform exception put an object there instead, carrying a platform error code:
{
"result": null,
"errors": true,
"error": {
"code": "P-01",
"error_message": "A Required Field is missing or is an invalid data type.",
"error_detailed": "number"
},
"message": "An error has occurred."
}
Handle both. A safe read is: if error is an object, use error.code and error.error_message; otherwise treat error as a string.
Platform Error Codes
The codes you can see through the v3 surface. They are stable identifiers; the accompanying text is not.
| Code | Meaning |
|---|---|
P-01 |
A required field is missing, or a field has the wrong type. error_detailed names the field. |
P-99 |
An unexpected error the platform did not classify. error_detailed carries the underlying exception text. Worth reporting to support with the timestamp. |
AST-13 |
The requested record was not found. |
AST-14 |
The requested phone number was not found on this account. |
AST-15 |
The request conflicts with current state. |
AST-16 |
The telephony backend is temporarily unavailable. Retry with backoff. |
HTTP Status Codes
| Status | What it means on this API |
|---|---|
| 200 | Success. |
| 400 | The request body or query is wrong: malformed JSON, a missing required field, a bad value. |
| 401 | No X-API-Key header, an unknown or revoked key, or a key type the endpoint cannot use to identify a user. |
| 403 | The key is valid but not allowed: the wrong account, a missing scope, the wrong key type, or a resource the key's user does not hold. |
| 404 | No such resource on this account. Also returned, deliberately, when the resource exists on a different account, so a 404 does not confirm an ID. |
| 409 | The request is valid but conflicts with current state or asks for something the platform will not do silently. See caller_id and recording on Call Management. |
| 500 | Unhandled server error. Retry once, then report it. |
| 503 | A dependency the request needs is not available. Retry with backoff. |
429 is listed in no table above on purpose. See Rate Limits.