zoxaAI
Homepage
API Reference

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/v1

All 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/json

The 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
}
ParameterDefaultMaxNotes
page1—1-indexed
per_page50100Capped 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:

OperationWhy
POST /call with type=inboundLast-write-wins upsert keyed by (provider, phoneNumber)
PATCH on any resourceReads then writes the same fields
DELETE on any resourceReturns 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

CodeMeaning
200Success (read)
201Success (resource created)
204Success (no body)
400Business-logic rejection (e.g., missing org, conflicting fields)
401Authentication missing or invalid
402Quota exhausted
403Forbidden (org mismatch, scope)
404Resource not found in your organization
409Uniqueness conflict
422Schema validation failed (Pydantic)
429Rate limit exceeded
500Server error (rare — please report)
501Feature requested isn't implemented for the chosen provider yet

API sections

SectionWhat lives here
CallsPlace outbound/inbound/web calls, list history, fetch detail
AgentsCreate and manage voice agents (LLM + voice + STT config)
ToolsFunction-calling tools — HTTP, end call, transfer, DTMF, knowledge base
Knowledge BaseRAG corpora, document ingestion, retrieval
FilesUpload assets used by tools, KB, campaigns
Inbound BindingsPhone numbers registered for inbound calls via the API-first flow
CampaignsBulk outbound calling, progress tracking, structured event logs
API KeysCreate and revoke keys for this organization. Mounted under /api/v1/user/api-keys.
UsageBilling-period usage summary, daily spend breakdown, per-day call detail. Mounted under /api/v1/wallet/dashboard and /api/v1/organizations/reports.
SchemasShared 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}.

On this page