Developers · Public REST API v1
The voxalways.com API
Originate outbound voice-AI calls, read post-call intelligence and receive signed webhooks the moment a call completes, programmatically, from any stack.
Base URL https://app.voxalways.com/api/v1Overview
The voxalways.com Public REST API lets you drive the voice platform from your own software: place outbound calls, poll their status, pull the saved transcript and post-call intelligence, and subscribe to webhooks for completion events. Every call you originate rides the same pipeline as the dashboard: the same trunk, quota, compliance and post-call analysis.
- REST over HTTPS; all request/response bodies are JSON.
- Responses are snake_case; timestamps are Unix epoch milliseconds.
- Every key is scoped to one tenant (your organization), so you only ever see your own data.
/api/v1. Don't have access yet?
Email hello@voxalways.com.
Authentication
Authenticate every request with a secret API key. Create keys in the dashboard under Settings → API keys. A key is shown in full exactly once at creation (it is stored only as a SHA-256 hash and cannot be recovered), so copy it then. Keys look like:
vx_live_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4
Pass it on every request in either header (Bearer is preferred):
Authorization: Bearer vx_live_...
# or
X-API-Key: vx_live_...
Scopes
Each key carries a coarse scope set. A request to an endpoint whose scope the key lacks returns 403.
| Scope | Grants |
|---|---|
| calls:read | List calls, retrieve a call, fetch transcripts. |
| calls:write | Originate outbound calls. |
Rate limits
Each API key is limited to 120 requests per 60 seconds
(fixed window). Exceeding it returns 429 Too Many Requests with a
Retry-After header (seconds to wait):
HTTP/1.1 429 Too Many Requests
Retry-After: 42
Content-Type: application/json
{ "error": "Rate limit exceeded" }
Back off for the indicated number of seconds, then retry.
Errors
Errors return the matching HTTP status and a JSON body with an error message (plus occasional context fields, e.g. blocked for a compliance stop):
{ "error": "This number is on the DND/suppression list and cannot be called.", "blocked": "dnd" }
| Status | Meaning |
|---|---|
200 | OK (GET). |
202 | Accepted: the call is being placed. |
400 | Validation error (e.g. malformed phone number). |
401 | Missing, invalid or revoked API key. |
403 | Key lacks the required scope, or a compliance gate blocked the call. |
404 | Resource not found for this tenant. |
429 | Rate limit exceeded. |
500 | Server error (e.g. SIP trunk not configured). |
Originate a call
Place an outbound call. The call is dispatched asynchronously and the endpoint
returns immediately with 202. Watch it complete by polling
GET /calls/{id} or subscribing to the
call.completed webhook. Quota, calling-window
and DND/suppression checks apply exactly as in the dashboard.
Body parameters
| Field | Type | Description | |
|---|---|---|---|
phoneNumber | string | required | Number to dial. E.164 or a national number with common separators. |
agentId | string | optional | Saved agent persona to use for this call. |
systemPrompt | string | optional | Inline system prompt that overrides the agent's default. |
agentName | string | optional | Display name for the agent on this call. |
prompt | string | optional | Per-call context / instructions for the agent. |
voice | string | optional | Voice id (e.g. priya). |
language | string | optional | Language code (e.g. en, hi). |
contactName | string | optional | Caller's name, stored on the call. |
categoryId | string | optional | Vertical/category for post-call field extraction. |
fieldValues | object | optional | Values for the agent's tool/category fields. |
campaign | string | optional | Campaign name (denormalized onto the call). |
campaignId | string | optional | Campaign id to attribute this call to. |
maxDuration | number | optional | Hard cap on call length, in minutes. |
allowTransfer | boolean | optional | Permit the agent to transfer to a human. |
retry | boolean | optional | Hint to retry on no-answer (carried as metadata). |
Request
curl -X POST "https://app.voxalways.com/api/v1/calls" \
-H "Authorization: Bearer vx_live_..." \
-H "Content-Type: application/json" \
-d '{
"phoneNumber": "+919876543210",
"agentId": "a_front_desk",
"contactName": "Alice",
"campaign": "Q3 Outreach",
"language": "en"
}'
Response · 202 Accepted
{
"id": "call-919876543210-5f8c",
"room": "call-919876543210-5f8c",
"status": "dispatched",
"recording": true
}
403 with "blocked": "dnd" means the number is on your
suppression list; a 403 with a scope message means the key can't write calls.
List calls
Return recent calls for your tenant, newest first, with post-call intelligence once analyzed.
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
page | number | 1 | 1-based page number. |
page_size | number | 25 | Rows per page (max 100). |
Request
curl "https://app.voxalways.com/api/v1/calls?page=1&page_size=25" \
-H "Authorization: Bearer vx_live_..."
Response · 200 OK
{
"data": [
{
"id": "call-919876543210-5f8c",
"room": "call-919876543210-5f8c",
"phone_number": "+919876543210",
"contact_name": "Alice",
"direction": "outbound",
"status": "completed",
"created_at": 1719000000000,
"ended_at": 1719001200000,
"summary": "Caller confirmed the appointment and asked about parking.",
"sentiment": "positive",
"outcome": "booked",
"recording_url": "https://.../call-919876543210-5f8c.ogg",
"cost_usd": 0.048,
"agent_name": "Ava (Front Desk)"
}
],
"page": 1,
"page_size": 25,
"total": 247,
"has_more": true
}
Retrieve a call
Fetch one call. Poll this after originating to watch it move
dispatched → active → completed. The intelligence fields
(summary, sentiment, outcome) fill in a few
seconds after the call ends, once post-call analysis runs.
Call object
| Field | Type | Description |
|---|---|---|
id | string | Call id. |
room | string | Internal room name. |
phone_number | string | The dialed number. |
contact_name | string | null | Caller name, if provided. |
direction | string | null | outbound or inbound. |
status | string | dispatched · active · completed · failed. |
created_at | number | Start time (epoch ms). |
ended_at | number | null | End time (epoch ms). |
summary | string | null | AI summary (after analysis). |
sentiment | string | null | positive · neutral · negative. |
outcome | string | null | interested · not_interested · callback · booked · no_answer · unknown. |
recording_url | string | null | Recording URL, if recording is enabled. |
cost_usd | number | null | Estimated call cost (USD). |
agent_name | string | null | Agent used on the call. |
Request
curl "https://app.voxalways.com/api/v1/calls/call-919876543210-5f8c" \
-H "Authorization: Bearer vx_live_..."
Response · 200 OK (while active)
{
"id": "call-919876543210-5f8c",
"status": "active",
"summary": null,
"sentiment": null,
"outcome": null,
"recording_url": "https://.../call-919876543210-5f8c.ogg",
"agent_name": "Ava (Front Desk)"
}
Get transcript
Return the saved transcript, ordered by seq. lines is
empty until the agent has persisted turns (or if transcript saving is off for the tenant).
Request
curl "https://app.voxalways.com/api/v1/calls/call-919876543210-5f8c/transcript" \
-H "Authorization: Bearer vx_live_..."
Response · 200 OK
{
"id": "call-919876543210-5f8c",
"lines": [
{ "role": "agent", "text": "Hello! This is Ava from Meridian Clinics...", "seq": 0, "at": 1719000010000 },
{ "role": "caller", "text": "Hi, yes, that time still works.", "seq": 1, "at": 1719000016000 }
]
}
Webhooks
Instead of polling, subscribe an HTTPS endpoint under
Settings → Webhooks and voxalways.com will POST an event to it.
The signing secret (whsec_...) is shown once at creation, so store it to
verify signatures. Available event types:
| Event | Fires when |
|---|---|
| call.completed | A call ended and post-call analysis finished (summary, sentiment, outcome, lead snapshot ready). |
| call.failed | A call failed (e.g. unanswered after retries). |
| recording.ready | A call's recording URL is available. |
| lead.created | A new lead was created from a call. |
| lead.updated | A lead's status or intelligence changed. |
| campaign.completed | A campaign run finished. |
| transfer.created | A call was transferred to a human. |
| whatsapp.reply.received | An inbound WhatsApp reply arrived. |
Headers
Content-Type: application/json
User-Agent: Voxalways-Webhooks/1.0
X-Voxalways-Event: call.completed
X-Voxalways-Delivery: whd_1a2b3c...
X-Voxalways-Signature: t=1719001200,v1=6b6f...9c1a
Example: call.completed payload
{
"id": "whd_1a2b3c4d5e",
"event": "call.completed",
"createdAt": 1719001200000,
"data": {
"call": {
"id": "call-919876543210-5f8c",
"room": "call-919876543210-5f8c",
"phoneNumber": "+919876543210",
"direction": "outbound",
"contactName": "Alice",
"campaign": "Q3 Outreach",
"campaignId": "cmp_1720000001",
"agentName": "Ava (Front Desk)",
"status": "completed",
"error": null,
"createdAt": 1719000000000,
"endedAt": 1719001200000,
"durationSeconds": 1200,
"summary": "Caller confirmed the appointment and asked about parking.",
"sentiment": "positive",
"outcome": "booked",
"intel": { "appointment": "Tomorrow · 2:15 PM", "branch": "Indiranagar" },
"recordingUrl": "https://.../call-919876543210-5f8c.ogg",
"transcriptUrl": "https://app.voxalways.com/transcripts?room=call-919876543210-5f8c"
},
"lead": {
"id": "ld_1720000010",
"phone": "+919876543210",
"name": "Alice",
"status": "booked",
"score": 88,
"intel": { "appointment": "Tomorrow · 2:15 PM" },
"callsCount": 1,
"nextFollowUpAt": null,
"updatedAt": 1719001200000
}
}
}
data.lead is null for calls that don't map to a lead.
Webhook payloads use the platform's native camelCase call shape
(the REST read endpoints above are snake_case).
Verifying signatures
Every delivery is signed Stripe-style. The X-Voxalways-Signature
header is t=<unix-seconds>,v1=<hex>, where
v1 = HMAC-SHA256(secret, "<t>.<raw-body>"). Verify against the
raw request body (not a re-serialized object), and reject if it doesn't match.
const crypto = require("crypto");
function verify(rawBody, sigHeader, secret) {
const parts = Object.fromEntries(sigHeader.split(",").map((kv) => kv.split("=")));
const expected = crypto
.createHmac("sha256", secret)
.update(parts.t + "." + rawBody)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}
// Express: capture the raw body with express.raw({ type: "application/json" }).
app.post("/webhooks/voxalways", express.raw({ type: "application/json" }), (req, res) => {
const ok = verify(req.body.toString(), req.headers["x-voxalways-signature"], process.env.VOX_WEBHOOK_SECRET);
if (!ok) return res.status(401).end();
const event = JSON.parse(req.body.toString());
// handle event.event / event.data ...
res.status(200).end();
});
The t value inside the signed string is a replay guard: reject deliveries whose timestamp is far from your current clock.
Delivery & retries
Return any 2xx within 10 seconds to acknowledge a
delivery. A non-2xx, a timeout, or a network error is retried on this schedule,
then marked exhausted:
| Attempt | When |
|---|---|
| 1 | immediately |
| 2 | +1 minute |
| 3 | +10 minutes |
| 4 | +1 hour |
| 5 | +6 hours |
| — | exhausted (no further retries) |
Make your handler idempotent: dedupe on X-Voxalways-Delivery (the
delivery id) since a slow-but-successful endpoint may occasionally see a retry.
Inspect and manually redeliver failed deliveries from
Settings → Webhooks → delivery log.