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

For individual users accessing their own data.

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

Only your own profile, numbers, and call history

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_...
Quick Comparison
Feature User Key Developer Key
Scope Single user Entire account
Access /user/* endpoints? ✅ Yes ❌ No
Access /queues, /availability? ❌ No ✅ Yes
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.

Rate Limit: 1000 requests per minute (applies to both key types)

Public Endpoints (No Authentication)

Limited endpoints available without authentication for status pages and public dashboards.

Available Endpoints:

  • User availability status (online/offline only)
  • Queue basic information
  • Queue availability (agent counts)

Rate Limit: 100 requests per minute per IP

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
  },
  "error": false,
  "message": "Success",
  "timestamp": "2024-01-15T10:30:00Z"
}
Error Response
{
  "result": null,
  "error": true,
  "message": "Invalid API key provided",
  "error_code": "AUTH_INVALID_KEY",
  "timestamp": "2024-01-15T10:30:00Z"
}

Response Fields

result object/array The actual response data. Will be null on error.
error boolean Indicates if the request was successful (false) or failed (true).
message string Human-readable message about the response.
error_code string Machine-readable error code (only present on errors).
timestamp string ISO 8601 timestamp of the response.

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.

Optional Auth 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 Array of active calls (requires authentication)

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,
          "direction": "outbound"
        }
      ]
    }
  ],
  "error": false,
  "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 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"
    },
    {
      "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"
    }
  ],
  "error": false,
  "message": "List of queues"
}
GET /v3/voice/{account_id}/queues/{queue_id}

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

Auth Required 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 Array of assigned queue members

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 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
    }
  ],
  "error": false,
  "message": "Queue availability data"
}
GET /v3/voice/{account_id}/availability/queues/{queue_id}

Get detailed availability for a specific queue.

Auth Required 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

Initiate an outbound call from an extension to an external number.

Auth Required Developer API Key

Request Body

Field Type Required Description
from string Required Extension number making the call
to string Required Destination phone number
caller_id string Optional caller ID to display
recording boolean Enable call recording (default: false)

Example Request

cURL
curl -X POST https://api.voice360.app/v3/voice/61/call/outbound \
  -H "X-API-Key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "1001",
    "to": "+15551234567",
    "caller_id": "+15559876543",
    "recording": true
  }'

Example Response

JSON
{
  "result": {
    "call_id": "c12345-67890-abcdef",
    "status": "initiating",
    "from": "1001",
    "to": "+15551234567",
    "timestamp": "2024-01-15T10:30:00Z"
  },
  "error": false,
  "message": "Call initiated successfully"
}

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
    }
  ],
  "error": false,
  "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
    }
  ],
  "error": false,
  "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"
  },
  "error": false,
  "message": "User profile details"
}

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.

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

API rate limits are enforced to ensure fair usage and maintain service quality for all users.

Authentication Type Rate Limit Window Headers Returned
API Key 1000 requests Per minute X-RateLimit-Limit, X-RateLimit-Remaining
No Auth (Public) 100 requests Per minute per IP X-RateLimit-Limit, X-RateLimit-Remaining
Need higher limits? Contact our sales team at sales@voice360.app to discuss enterprise rate limits.

Error Handling

The API uses standard HTTP status codes to indicate success or failure of requests.

Status Code Error Code Description
200 - Success - Request completed successfully
400 BAD_REQUEST Invalid request parameters or malformed request
401 AUTH_REQUIRED Authentication required but not provided
403 AUTH_INVALID Invalid API key or insufficient permissions
404 NOT_FOUND Requested resource not found
429 RATE_LIMITED Too many requests - rate limit exceeded
500 INTERNAL_ERROR Internal server error - please try again
503 SERVICE_UNAVAILABLE Service temporarily unavailable

Error Response Example

JSON
{
  "result": null,
  "error": true,
  "message": "Invalid API key provided",
  "error_code": "AUTH_INVALID",
  "details": {
    "provided_key": "partial_key_xxx...",
    "help": "Please check your API key at https://voice360.app/settings/api"
  },
  "timestamp": "2024-01-15T10:30:00Z"
}