Overview
Base URL, request and response conventions, pagination, idempotency, and how the zoxaAI HTTP API is organized.
At a glance
JSON over HTTPS. One base URL. API-key auth on every request. Resources are organized by domain (calls, agents, tools, knowledge base, files, campaigns).
Base URL
https://dashboard.zoxa.ai/api/v1All endpoints in this reference are relative to that base URL. There is no separate region or sandbox host — production and staging use the same URL but different API keys.
Authentication
Every request must include an API key:
X-API-Key: zsk_a1b2c3d4...WebSocket endpoints accept the key as a query string (?api_key=...) because browser WebSocket handshakes cannot carry custom headers. See Authentication for full details.
Content type
All request and response bodies are JSON. Send Content-Type: application/json on every POST/PUT/PATCH.
POST /api/v1/call
X-API-Key: zsk_...
Content-Type: application/jsonThe only exception is POST /api/v1/files (binary upload) which uses multipart/form-data.
Field naming
Field casing is not uniform across the API — match each endpoint's own reference page:
- Agent-config and call bodies use camelCase (
callId,agentId,phoneNumber,systemPrompt). - Telephony, knowledge-base, campaign, and API-key bodies use snake_case (
inbound_agent_uuid,document_uuids,agent_uuid,source_type).
Query string parameters use snake_case (?from=...&to=..., ?agent_id=...). Path segments use kebab-case (/inbound-bindings/...).
Pagination
List endpoints accept page (1-indexed) and per_page query parameters and return a consistent envelope:
{
"items": [ /* array of resources */ ],
"total": 1234,
"page": 1,
"perPage": 50,
"totalPages": 25
}| Parameter | Default | Max | Notes |
|---|---|---|---|
page | 1 | — | 1-indexed |
per_page | 50 | 100 | Capped per endpoint; some are lower |
A small number of endpoints use limit + offset instead — those are flagged on the endpoint page.
Idempotency
The following operations are safe to retry on network failure without producing duplicates:
| Operation | Why |
|---|---|
POST /call with type=inbound | Last-write-wins upsert keyed by (provider, phoneNumber) |
PATCH on any resource | Reads then writes the same fields |
DELETE on any resource | Returns 204 whether the row existed or not (for bindings) |
POST /call with type=outbound is not idempotent — each call places a new outbound dial. Use your own client-side dedup key if you need exactly-once semantics.
Errors
Every error response follows the FastAPI standard shape:
{ "detail": "Human-readable error message" }For validation errors (HTTP 422), detail is an array of structured field errors. See Errors for the full status code table and validation error structure.
Common status codes
| Code | Meaning |
|---|---|
200 | Success (read) |
201 | Success (resource created) |
204 | Success (no body) |
400 | Business-logic rejection (e.g., missing org, conflicting fields) |
401 | Authentication missing or invalid |
402 | Quota exhausted |
403 | Forbidden (org mismatch, scope) |
404 | Resource not found in your organization |
409 | Uniqueness conflict |
422 | Schema validation failed (Pydantic) |
429 | Rate limit exceeded |
500 | Server error (rare — please report) |
501 | Feature requested isn't implemented for the chosen provider yet |
API sections
| Section | What lives here |
|---|---|
| Calls | Place outbound/inbound/web calls, list history, fetch detail |
| Agents | Create and manage voice agents (LLM + voice + STT config) |
| Tools | Function-calling tools — HTTP, end call, transfer, DTMF, knowledge base |
| Knowledge Base | RAG corpora, document ingestion, retrieval |
| Files | Upload assets used by tools, KB, campaigns |
| Inbound Bindings | Phone numbers registered for inbound calls via the API-first flow |
| Campaigns | Bulk outbound calling, progress tracking, structured event logs |
| API Keys | Create and revoke keys for this organization. Mounted under /api/v1/user/api-keys. |
| Usage | Billing-period usage summary, daily spend breakdown, per-day call detail. Mounted under /api/v1/wallet/dashboard and /api/v1/organizations/reports. |
| Schemas | Shared object shapes referenced from every endpoint |
A minimal end-to-end example
Create an agent, then dial out with it:
# 1. Create an agent
AGENT_ID=$(curl -s -X POST https://dashboard.zoxa.ai/api/v1/agents \
-H "X-API-Key: zsk_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Sales bot",
"systemPrompt": "You qualify leads for a B2B SaaS.",
"greeting": { "firstMessages": ["Hi, do you have a minute to chat?"] },
"llm": { "provider": "openai", "model": "gpt-5.4-mini" },
"tts": { "provider": "elevenlabs" },
"stt": { "provider": "soniox" }
}' | jq -r '.uuid')
# 2. Dial out
curl -X POST https://dashboard.zoxa.ai/api/v1/call \
-H "X-API-Key: zsk_..." \
-H "Content-Type: application/json" \
-d "{
\"type\": \"outbound\",
\"callConfig\": {
\"provider\": \"twilio\",
\"phoneNumber\": \"+14155550100\",
\"auth\": { \"accountSid\": \"AC...\", \"authToken\": \"...\" }
},
\"toNumber\": \"+14155559999\",
\"agentId\": \"$AGENT_ID\"
}"const headers = {
"X-API-Key": "zsk_...",
"Content-Type": "application/json",
};
const BASE = "https://dashboard.zoxa.ai/api/v1";
// 1. Create an agent
const agent = await fetch(`${BASE}/agents`, {
method: "POST",
headers,
body: JSON.stringify({
name: "Sales bot",
systemPrompt: "You qualify leads for a B2B SaaS.",
greeting: { firstMessages: ["Hi, do you have a minute to chat?"] },
llm: { provider: "openai", model: "gpt-5.4-mini" },
tts: { provider: "elevenlabs" },
stt: { provider: "soniox" },
}),
}).then((r) => r.json());
// 2. Dial out
const call = await fetch(`${BASE}/call`, {
method: "POST",
headers,
body: JSON.stringify({
type: "outbound",
callConfig: {
provider: "twilio",
phoneNumber: "+14155550100",
auth: { accountSid: "AC...", authToken: "..." },
},
toNumber: "+14155559999",
agentId: agent.uuid,
}),
}).then((r) => r.json());
console.log(call.callId);import httpx
BASE = "https://dashboard.zoxa.ai/api/v1"
client = httpx.Client(
base_url=BASE,
headers={"X-API-Key": "zsk_..."},
)
# 1. Create an agent
agent = client.post("/agents", json={
"name": "Sales bot",
"systemPrompt": "You qualify leads for a B2B SaaS.",
"greeting": {"firstMessages": ["Hi, do you have a minute to chat?"]},
"llm": {"provider": "openai", "model": "gpt-5.4-mini"},
"tts": {"provider": "elevenlabs"},
"stt": {"provider": "soniox"},
}).json()
# 2. Dial out
call = client.post("/call", json={
"type": "outbound",
"callConfig": {
"provider": "twilio",
"phoneNumber": "+14155550100",
"auth": {"accountSid": "AC...", "authToken": "..."},
},
"toNumber": "+14155559999",
"agentId": agent["uuid"],
}).json()
print(call["callId"])The call is now in-flight. Lifecycle and outcome land on the calls row — retrievable via GET /calls/{callId}.