VEXYL AI

HTTP API

The gateway has a small HTTP API for health checks, per-call settings, outbound calls and housekeeping. Every response is JSON. The examples use port 8081 on the same host; replace it with your HTTP_PORT.

Access

Variable Default Meaning
HTTP_HOST 127.0.0.1 Address the API listens on.
HTTP_PORT 8080 Port the API listens on. The fallback collides with AUDIOSOCKET_PORT, so set it explicitly (this manual uses 8081).
HTTP_ALLOWED_IPS unset Comma-separated exact IPs and IPv4 CIDR blocks. Unset means the whitelist is off and any client that can reach the port is served. A blocked request gets 403.
HTTP_TRUST_PROXY false Set true behind a reverse proxy so the client address is read from X-Forwarded-For.
HTTP_HOST=127.0.0.1
HTTP_PORT=8081
HTTP_ALLOWED_IPS=127.0.0.1,192.0.2.0/24

The API has no API key. Restrict it by IP, bind it to loopback, or put an authenticating proxy in front. Anyone who can reach it can create sessions and place outbound calls.

Health and licence

GET /health reports gateway state. status is ok or degraded. The billing block appears only when BILLING_CDR_ENABLED=true, and sipBridge only when the SIP bridge is on.

curl -s http://127.0.0.1:8081/health
{
  "status": "ok",
  "timestamp": "2026-10-24T07:19:33.032Z",
  "storage": { "storage_type": "memory", "redis_connected": false, "memory_sessions": 0, "default_ttl": 3600 },
  "telephonyMode": "both",
  "server": { "http_host": "0.0.0.0", "http_port": 8081, "audiosocket_host": "127.0.0.1", "audiosocket_port": 8080 },
  "concurrentCalls": { "active": 0, "max": 30, "utilization": "0%" },
  "circuitBreakers": { "litebot-chat": { "state": "CLOSED", "failures": 0, "successRate": "0%", "totalRequests": 0 } },
  "billing": { "enabled": true, "pending": 2, "dead": 2, "last_delivery_at": "2026-10-23T15:52:10.435Z", "last_error": "HTTP 400" },
  "sipBridge": { "active": true, "mode": "sip_bridge", "sessions": 0, "sipPort": "5080", "registered": false, "registerUri": null }
}

concurrentCalls.max is your licence cap. Counter values here are examples.

GET /license shows the licence in force. The free tier shows tier FREE, concurrentCalls 4, licensee null and isFree true. expiry is an ISO date or "Never". machineId equals the output of --get-machine-id; see Licence.

curl -s http://127.0.0.1:8081/license
{ "tier": "PROFESSIONAL", "tierName": "Professional", "licensee": "Example Co", "concurrentCalls": 30, "expiry": "Never", "machineId": "<id>", "isFree": false, "valid": true }

Session metadata

Metadata is per-call information the gateway uses for that call: who is calling, which bot answers, which language and voice. Send it before the call reaches the gateway. For the Asterisk dialplan that sends it, see Send caller details first; for SIP, see SIP trunk.

POST /session/{uuid}/metadata takes a JSON body or a form-encoded body (Asterisk CURL() sends form). It merges into the session, so call it again to update.

curl -s -X POST http://127.0.0.1:8081/session/0b9d6c52-1f3a-4c1e-9a77-2f6e5d1c8a10/metadata \
  -H 'Content-Type: application/json' \
  -d '{"caller_id":"+9198xxxxxxxx","caller_name":"Priya","botId":"sales-followup","language_code":"en-IN"}'
{ "success": true, "message": "Session metadata stored", "session_uuid": "0b9d6c52-1f3a-4c1e-9a77-2f6e5d1c8a10" }

GET /session/{uuid}/metadata returns the stored object, or 404 if there is none. DELETE /session/{uuid} removes the session and returns { "success": true, "message": "Session deleted" }.

curl -s http://127.0.0.1:8081/session/0b9d6c52-1f3a-4c1e-9a77-2f6e5d1c8a10/metadata
curl -s -X DELETE http://127.0.0.1:8081/session/0b9d6c52-1f3a-4c1e-9a77-2f6e5d1c8a10

Accepted keys

Key Meaning
caller_id Caller number. Form alias callerid. Default "Unknown".
caller_name Caller name. Form alias name.
channel Asterisk channel name. Required for transfers on AudioSocket calls.
language_code Language for this call. Default ml-IN.
destination_number The number (DID) that was called.
botId LiteBot bot for this call. Case-sensitive; an unknown id uses the default bot.
tts_provider Speech provider for this call: sarvam, google, gemini, elevenlabs, deepgram, azure, murf, vexyl, smallest or openrouter.
stt_provider Recognition provider: sarvam, groq, gemini, openai, deepgram, google or vexyl-stt.
tts_model Per-call model. Only openrouter and gemini read it.
tts_voice Per-call voice. Read only by the provider named in tts_provider.
realtimeProvider litebot, gemini-live, or off (also pipeline) to use the normal pipeline.
greeting_mode Outbound only: static, dynamic, hybrid or wait.
initial_greeting_audio Outbound only: path to a WAV greeting.
llm_context Free text handed to the AI brain as background.
any other key Passed to the brain as context.

Id rules for the per-call TTS keys:

Key Accepted form
tts_model vendor/name or a bare id. Each part is 1 to 64 characters of A-Za-z0-9_.-; the second part may also contain :.
tts_voice 1 to 160 characters of A-Za-z0-9_.:-.

An invalid tts_provider, stt_provider, tts_model or tts_voice is dropped with a warning in the log and the request still succeeds. The call then uses the host default, so check the log if a voice does not change. Browser widget sessions cannot set these keys.

Outbound calls

Outbound calls need these variables in .env:

Variable Default Meaning
OUTBOUND_ENABLED off Set true to open the outbound routes; otherwise they return 403.
OUTBOUND_ORIGINATE_ENDPOINT unset Your webhook that places the call. The gateway posts the session id, phone number, language, a timestamp and the request metadata to it. Without it the session is created and the reply carries a warning.
OUTBOUND_TIMEOUT 10000 Milliseconds to wait for that webhook.
OUTBOUND_GREETING_MODE static Default greeting mode.

SIP bridge hosts place the call themselves; see Outbound SIP calls. On AudioSocket hosts your originate webhook, for example one that talks to your PBX, places it.

POST /outbound/initiate starts a call.

Field Meaning
phoneNumber Required. Destination number.
language Language code. Default is DEFAULT_LANGUAGE, else ml-IN.
greetingMode static, dynamic, hybrid or wait.
initialGreeting Greeting text.
audioFile Path to a WAV greeting.
webhookUrl URL for status callbacks.
tts_provider, tts_model, tts_voice, stt_provider, botId Per-call overrides, same rules as session metadata.
metadata Object with botId, caller_name, llm_context, language_code, the TTS keys and any custom fields.
curl -s -X POST http://127.0.0.1:8081/outbound/initiate \
  -H 'Content-Type: application/json' \
  -d '{"phoneNumber":"+9198xxxxxxxx","language":"en-IN","greetingMode":"dynamic","metadata":{"botId":"sales-followup","caller_name":"Priya","llm_context":"Follow-up on a demo request."}}'
{ "sessionUuid": "550e8400-e29b-41d4-a716-446655440000", "status": "initiated", "message": "Outbound call webhook triggered successfully" }

GET /outbound/status/{uuid} returns the call state: initiated, answered, failed or cancelled.

curl -s http://127.0.0.1:8081/outbound/status/550e8400-e29b-41d4-a716-446655440000
{
  "sessionUuid": "550e8400-e29b-41d4-a716-446655440000",
  "status": "answered",
  "phoneNumber": "+9198xxxxxxxx",
  "language": "en-IN",
  "initiatedAt": "2026-10-24T12:00:00.000Z",
  "answeredAt": "2026-10-24T12:00:15.000Z",
  "completedAt": null,
  "metadata": { "call_direction": "outbound", "destination_number": "+9198xxxxxxxx", "greeting_mode": "dynamic", "language_code": "en-IN", "botId": "sales-followup", "caller_name": "Priya" }
}

POST /outbound/cancel/{uuid} marks the session cancelled. It returns 404 for an unknown id and 400 if the call was already answered or completed. It does not hang up a ringing leg: that is your originate webhook's job.

curl -s -X POST http://127.0.0.1:8081/outbound/cancel/550e8400-e29b-41d4-a716-446655440000
{ "success": true, "message": "Call marked as cancelled", "sessionUuid": "550e8400-e29b-41d4-a716-446655440000" }

Active calls and stats

GET /calls/stats shows counters since the process started and the calls in progress.

curl -s http://127.0.0.1:8081/calls/stats
{ "activeCalls": 0, "maxCalls": 30, "tier": "Professional", "totalHandled": 12, "totalRejected": 0, "peakConcurrent": 1, "utilizationPercent": 0, "activeCallDetails": [] }

GET /calls/active returns the same call list with a count and the cap.

curl -s http://127.0.0.1:8081/calls/active
{ "count": 0, "max": 30, "calls": [] }

TTS cache

GET /cache/stats shows the cache hit rate since the process started. hitRateChars tracks the speech bill best, because a hit on a short word and a miss on a long sentence are not equal. Per-call figures are in the billing records.

curl -s http://127.0.0.1:8081/cache/stats
{
  "cache": {
    "enabled": true,
    "hits": 80,
    "misses": 40,
    "errors": 0,
    "hitRate": "66.7%",
    "totalSizeMB": "4.10",
    "entryCount": 120,
    "lastCleanup": "2026-01-01T09:00:00.000Z",
    "estimatedSavings": { "apiCallsSaved": 80, "estimatedCostUSD": "0.24" }
  },
  "byProvider": {
    "google": { "hits": 80, "misses": 40, "hitChars": 3900, "missChars": 2800 }
  },
  "hitRateRequests": "66.7%",
  "hitRateChars": "58.2%",
  "sinceProcessStart": true
}

POST /cache/warmup renders phrases ahead of time so the first caller does not wait for them. It returns 400 unless TTS_CACHE_ENABLED=true. See The TTS cache.

curl -s -X POST http://127.0.0.1:8081/cache/warmup \
  -H 'Content-Type: application/json' \
  -d '{"phrases":[{"text":"Hello, how can I help you today?","languageCode":"en-IN"}]}'
{ "success": true, "cached": 1, "skipped": 0, "errors": 0, "total": 1 }

Billing replay

POST /billing/replay retries billing records that failed to deliver. Use {"scope":"dead"} after fixing the billing endpoint or its token: it moves every dead-lettered record back to pending and sweeps. {"scope":"pending"} (the default) forces an immediate retry sweep. It returns 400 unless BILLING_CDR_ENABLED=true. See Webhooks and billing.

curl -s -X POST http://127.0.0.1:8081/billing/replay \
  -H 'Content-Type: application/json' -d '{"scope":"dead"}'
{ "scope": "dead", "moved": 2, "pending": 2, "dead": 0 }

Circuit breakers

When a provider fails repeatedly the gateway stops calling it for a while and uses a fallback. GET /circuit-breakers shows each breaker. State is CLOSED (normal), OPEN (provider skipped) or HALF_OPEN (testing). Breakers appear as providers are used. nextAttemptIn says when an OPEN breaker retries.

curl -s http://127.0.0.1:8081/circuit-breakers
{ "litebot-chat": { "name": "litebot-chat", "state": "CLOSED", "failures": 0, "successRate": "0%", "totalRequests": 0, "lastFailureTime": null, "lastErrorMessage": null, "nextAttemptIn": null } }

POST /circuit-breakers/reset resets one breaker by name (404 if unknown). An empty body resets all.

curl -s -X POST http://127.0.0.1:8081/circuit-breakers/reset \
  -H 'Content-Type: application/json' -d '{"name":"litebot-chat"}'
{ "success": true, "message": "Circuit breaker litebot-chat reset successfully" }

Hosted customers

This API is not exposed on a hosted service. See Hosted service.