Voice360 API Documentation

Build powerful integrations with our comprehensive REST API

RESTful API
Secure Authentication
Real-time Data
99.9% Uptime

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.

Authentication is moving to headers! Starting with v3, all authentication tokens should be passed in request headers rather than URL parameters for improved security.

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.

Best For:
  • Mobile apps
  • Browser extensions
  • Personal integrations
Access:

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.

Key Format: abc123xyz...
Developer API Key

For account-level access to all data.

Best For:
  • Backend integrations
  • Dashboards
  • CRM integrations
Access:

All users, queues, and account-wide data

Key Format: 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
HTTP Header (Both Key Types)
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:

401 Unauthorized
{
  "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

Base URL
https://api.voice360.app/v3/voice
All API requests must be made over HTTPS. Calls made over plain HTTP will be redirected to HTTPS automatically.

Response Format

All API responses follow a consistent JSON structure for predictable integration:

Standard Response
{
  "result": {
    // Response data goes here
  },
  "errors": false,
  "error": null,
  "message": "Success"
}
Error Response
{
  "result": null,
  "errors": true,
  "error": "API key not found or invalid",
  "message": "Invalid API key"
}

Response Fields

result object/array/null The response data. Null on error.
errors boolean True when the request failed, false when it succeeded. Note the plural. This is the field to branch on.
error string/object/null Null on success. On failure it is either a short string naming what went wrong, or an object carrying a platform error code. See Error Handling.
message string Human-readable message about the response.
The boolean is 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 /v3/voice/{account_id}/availability

Get real-time availability status for all users in an account. Shows online/offline state and active calls.

A User API Key works here and returns the whole account, not just that key's own user. If you are giving a user key to a third party, that is what they can see.
Auth Required User API Key Developer API Key

Parameters

Parameter Type Required Description
account_id integer Required Your Voice360 account ID

Response Fields

pk integer User ID
presence_id string User's extension number
callerID string User's display name
first_name string User's first name
last_name string User's last name
devices_registered boolean Online status (true = online, false = offline)
channels array The user's active calls. Each entry has 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

JSON
{
  "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

GET /v3/voice/{account_id}/queues

List all queues in your account. Internal system queues are automatically filtered out.

Auth Required User API Key Developer API Key

Response Example

JSON
{
  "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 /v3/voice/{account_id}/queues/{queue_id}

Get detailed information about a specific queue including configuration and assigned users.

Auth Required User API Key Developer API Key

Response Fields

pk integer Queue ID
name string Queue identifier (e.g. "100134")
queue_name string User-friendly queue name
extension string Queue extension number
strategy string Ring strategy (ringall, fewestcalls, random, etc.)
musiconhold string Music on hold class
timeout integer Ring timeout in seconds
retry integer Retry interval in seconds
wrapuptime integer Wrap-up time in seconds
maxlen integer Maximum queue length (0 = unlimited)
joinempty string Allow joining empty queue
leavewhenempty string Leave when queue becomes empty
ringinuse string Ring members already on calls
assigned_users array The queue's members, one entry per assigned user. 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

JSON
{
  "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 /v3/voice/{account_id}/availability/queues

Get real-time agent availability metrics for all queues. Shows online agents and call activity.

Auth Required User API Key Developer API Key

Response Example

JSON
{
  "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 /v3/voice/{account_id}/availability/queues/{queue_id}

Get detailed availability for a specific queue.

Auth Required User API Key Developer API Key

Response Fields

pk integer Queue ID
name string Queue ID/number
queue_name string User-friendly queue name
extension string Queue extension number
is_ready boolean Queue has at least one online agent
agents_online integer Total agents logged in
agents_available integer Agents online but not on calls
agents_on_call integer Agents currently on calls
active_calls integer Active calls in this queue

Call Management

POST /v3/voice/{account_id}/call/outbound

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.

Auth Required Developer API Key User API Key Scope: voice:write

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.

Automatic caller ID. If the user has it enabled, the platform may present a different number that the account also owns, chosen by matching the destination's area code. That number is still owned by the account, so the attestation guarantee holds, but the exact number cannot be predicted at request time. When this applies, the response message says so.

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 recording and 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
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

200 OK
{
  "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

call_id string Identifier for this click-to-call, in the form c2c-<epoch seconds>. It is the platform's handle on the origination, not a CDR ID.
status string Always "initiating". The response returns before the first leg rings, so a 200 means the call was accepted and started, not that anyone answered.
caller_id string The number that will be presented, subject to the automatic caller ID caveat above.
recording boolean Whether this call will be recorded. Always present, and always the truth, whether or not you sent the field.
ring string "device" or "cell", echoing what was used.
user_id integer The resolved Voice360 user the call was placed as.

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

POST /v3/voice/{account_id}/sms/send

Send an SMS message from your Voice360 phone number.

Auth Required User API Key Developer API Key

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

JavaScript
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

GET /v3/voice/{account_id}/numbers

List all phone numbers (DIDs) in the account. Returns every number regardless of user assignment. Use query parameters to filter results.

Auth Required Developer API Key Only

Query Parameters

number string Filter by phone number (E.164 format)
owner_id string Filter by owner user ID
status string Filter by number status
sms_assigned_user_id string Filter by SMS assigned user ID
ai_agent_id string Filter by AI agent assignment

Response Fields

pk integer Number ID
number string Phone number in E.164 format
friendly_name string User-friendly display name
sms_enabled boolean Whether SMS is enabled for this number
mms_enabled boolean Whether MMS is enabled for this number

Example Response

JSON
{
  "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

JavaScript
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 /v3/voice/{account_id}/user/numbers

Get list of phone numbers (DIDs) assigned to the authenticated user for making calls and sending SMS.

Auth Required User API Key Only

Response Fields

pk integer Number ID
number string Phone number in E.164 format
friendly_name string User-friendly display name
sms_enabled boolean Whether SMS is enabled for this number
mms_enabled boolean Whether MMS is enabled for this number

Example Response

JSON
{
  "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

JavaScript
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 /v3/voice/{account_id}/user/profile

Get the authenticated user's profile and extension details including voicemail settings, call forwarding, and device information.

Auth Required User API Key Only

Response Fields

pk integer User ID
presence_id string User's extension number
callerID string Display name for outbound calls
first_name string User's first name
last_name string User's last name
email string User's email address
voicemail_enabled boolean Whether voicemail is enabled
forward_enabled boolean Whether call forwarding is enabled
forward_number string Number to forward calls to

Example Response

JSON
{
  "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.

Custom functions (agent tools) live inside the agent config. To add, edit, or remove a webhook-based tool the agent can call mid-conversation, update 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.
GET /v3/ai/{account_id}/agents

List AI agents on the account.

Auth Required Developer API Key Only Scope: ai:read

Query Parameters

name string Filter by agent name (partial match)
bot_type string Filter by agent type
offset / limit integer Pagination (limit max 200, default 50)

Example Request

JavaScript
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);
};
POST /v3/ai/{account_id}/agents

Create a new AI agent. account_id is always taken from the URL, not the request body.

Auth Required Developer API Key Only Scope: ai:write

Request Body

FieldTypeRequiredDescription
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 /v3/ai/{account_id}/agents/{agent_id}

Get an agent's full configuration, including modern_config (model, STT/TTS/voice, capabilities, custom functions).

Auth Required Developer API Key Only Scope: ai:read
PATCH /v3/ai/{account_id}/agents/{agent_id}

Partially update an agent. Send only the fields you're changing — this is also how you add or edit custom function tools.

Auth Required Developer API Key Only Scope: ai:write

Example Request — Add a Booking Tool

JavaScript
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 /v3/ai/{account_id}/agents/{agent_id}

Delete an agent.

Auth Required Developer API Key Only Scope: ai:write
POST /v3/ai/{account_id}/agents/{agent_id}/duplicate

Clone an agent (new agent named "<original> (Copy)").

Auth Required Developer API Key Only Scope: ai:write
POST /v3/ai/{account_id}/agents/{agent_id}/regenerate-prompts

Rebuild call_prompt / message_prompt from modern_config.

Auth Required Developer API Key Only Scope: ai:write

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.

GET /v3/ai/{account_id}/agents/{agent_id}/knowledge-base/documents

List documents, including processing/embedding status.

Auth Required Developer API Key Only Scope: ai:read
POST /v3/ai/{account_id}/agents/{agent_id}/knowledge-base/documents

Upload a document (multipart form, field name file).

Auth Required Developer API Key Only Scope: ai:write
GET /v3/ai/{account_id}/agents/{agent_id}/knowledge-base/documents/{document_id}

Get a single document's metadata and processing status.

Auth Required Developer API Key Only Scope: ai:read
DELETE /v3/ai/{account_id}/agents/{agent_id}/knowledge-base/documents/{document_id}

Delete a document.

Auth Required Developer API Key Only Scope: ai:write
GET /v3/ai/{account_id}/agents/{agent_id}/knowledge-base/notes

List text notes. Filters: category, enabled_only.

Auth Required Developer API Key Only Scope: ai:read
POST /v3/ai/{account_id}/agents/{agent_id}/knowledge-base/notes

Create a text note.

Auth Required Developer API Key Only Scope: ai:write

Request Body

FieldTypeRequiredDescription
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
PUT /v3/ai/{account_id}/agents/{agent_id}/knowledge-base/notes/{note_id}

Update a text note (partial).

Auth Required Developer API Key Only Scope: ai:write
DELETE /v3/ai/{account_id}/agents/{agent_id}/knowledge-base/notes/{note_id}

Delete a text note.

Auth Required Developer API Key Only Scope: ai:write

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

GET /v3/ai/{account_id}/calls

List AI call records. Filters: ai_agent_id, call_direction, resource_type.

Auth Required Developer API Key Only Scope: ai:read
GET /v3/ai/{account_id}/calls/{call_uuid}

Get a single AI call record.

Auth Required Developer API Key Only Scope: ai:read
GET /v3/ai/{account_id}/call-timelines

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

Auth Required Developer API Key Only Scope: ai:read
GET /v3/ai/{account_id}/call-timelines/{aicall_uuid}

Get a single call timeline.

Auth Required Developer API Key Only Scope: ai:read
GET /v3/ai/{account_id}/grading

List AI call grades. Filters: resource_type, grade, start_date, end_date.

Auth Required Developer API Key Only Scope: ai:read
GET /v3/ai/{account_id}/grading/{aicall_uuid}

Get the grade for a single call.

Auth Required Developer API Key Only Scope: ai:read

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:

MethodEndpointScopeDescription
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

  1. Configure a webhook in your Voice360 portal with your endpoint URL and event type
  2. When the event occurs, Voice360 sends an HTTP POST request to your URL with event data
  3. Your server processes the webhook payload and responds with 200 OK
  4. 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.

JSON
{
  "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

JSON
{
  "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

JSON
{
  "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

JSON
{
  "event_name": "USER_DIAL",
  "account_id": "12345",
  "timestamp": 1704067200,
  "metadata": {
    "dialed_number": "+15551234567",
    "from_user_id": "67890",
    "uniqueid": "1704067200.12345"
  }
}

USER_ANSWER

JSON
{
  "event_name": "USER_ANSWER",
  "account_id": "12345",
  "timestamp": 1704067200,
  "metadata": {
    "caller_id_number": "+15551234567",
    "to_user_id": "67890",
    "uniqueid": "1704067200.12345"
  }
}

USER_HANGUP

JSON
{
  "event_name": "USER_HANGUP",
  "account_id": "12345",
  "timestamp": 1704067200,
  "metadata": {
    "dialed_number": "+15551234567",
    "from_user_id": "67890",
    "uniqueid": "1704067200.12345",
    "duration": 45
  }
}

SMS_RECEIVED

JSON
{
  "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

JSON
{
  "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

JSON
{
  "event_name": "SMS_DELIVERED",
  "account_id": "12345",
  "timestamp": 1704067200,
  "metadata": {
    "to_number": ["+15551234567"],
    "message_id": "msg_xyz789",
    "status": "delivered"
  }
}

QUEUE_ENTER

JSON
{
  "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

JSON
{
  "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

JSON
{
  "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

JSON
{
  "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

JSON
{
  "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

JSON
{
  "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

JSON
{
  "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

JSON
{
  "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

JSON
{
  "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

JSON
{
  "event_name": "CONTACT_UPDATED",
  "account_id": "12345",
  "timestamp": 1704067200,
  "metadata": {
    "contact_id": "98765",
    "updated_fields": ["first_name", "email"],
    "phone": "+15551234567"
  }
}

CONTACT_DELETED

JSON
{
  "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.

JSON
{
  "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:

JSON
{
  "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"
}
On transcription failure: the event still fires with 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:

HTTP
X-Voice360-Signature: sha256=<64-char-hex-hmac>

Verification (Node.js):

JavaScript
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):

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
Important: Verify against the raw request body bytes, not the parsed JSON. Re-serializing the parsed payload may change whitespace or key order, which will invalidate the signature. Most frameworks expose the raw body via something like 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-Signature header 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.

bash
#!/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
  }'
JavaScript
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();
Python
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
<?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";
}

?>
C#
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}");
        }
    }
}
Java
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

No request rate limit is currently enforced on the v3 API. There is no per-key or per-IP request quota, at the gateway or in the application. Responses carry no 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/send has 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.
Planning a high-volume integration? Tell us the traffic shape before you build it, at sales@voice360.app. Knowing about it in advance is how it stays unlimited.

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.

There is no 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:

401 Unauthorized
{
  "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:

400 Bad Request
{
  "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.