Skip to main content

Overview

The Voxray server exposes a JSON/HTTP REST API for health, WebRTC signaling, session management, and Prometheus metrics, plus WebSocket and telephony transports for real-time voice pipelines. All JSON endpoints follow a consistent success and error envelope. The interactive OpenAPI specification is available at /swagger/ when the server is running. The spec file (swagger.json) is located in the docs/ folder; Mintlify renders it automatically when configured in mint.json.

Base URL

The default bind address is localhost on port 3042. Override both with configuration or environment variables.
http://localhost:3042
Config KeyEnv VariableDefaultEffect
hostHOST / VOXRAY_HOSTlocalhostServer bind address
portPORT / VOXRAY_PORT3042Server listen port
tls_enableVOXRAY_TLS_ENABLEfalseSwitches to HTTPS when true
When tls_enable is true, the server calls ListenAndServeTLS and the base URL becomes https://.

Versioned paths

All core routes are available at both a versioned prefix and a legacy path:
VersionedLegacy
GET /api/v1/healthGET /health
GET /api/v1/readyGET /ready
POST /api/v1/webrtc/offerPOST /webrtc/offer
POST /api/v1/startPOST /start
POST /api/v1/sessions/{id}/offerPOST /sessions/{id}/api/offer
PATCH /api/v1/sessions/{id}/offerPATCH /sessions/{id}/api/offer
Prefer the versioned paths for all new integrations. Legacy paths remain supported but are not guaranteed in future major versions.

Authentication

Authentication is optional and controlled entirely by the server_api_key configuration value (or the VOXRAY_SERVER_API_KEY environment variable). When this key is not set, all endpoints are open. When a key is configured, protected endpoints require one of the following request headers:
Authorization: Bearer <key>
X-API-Key: <key>
Both header forms are accepted interchangeably. The key is static — there is no login, token exchange, expiry, or refresh flow. On failure: 401 Unauthorized with error code UNAUTHORIZED.

Protected vs open endpoints

EndpointAuth Required
GET /health, GET /readyNo
GET /metricsNo
GET /swagger/No
POST /webrtc/offerYes (when key configured)
POST /startYes (when key configured)
POST /sessions/{id}/api/offerYes (when key configured)
PATCH /sessions/{id}/api/offerYes (when key configured)
GET /wsYes (when key configured)
POST / (telephony webhook)No
GET /telephony/wsNo
POST /daily-dialin-webhookOptional X-Webhook-Secret
The /daily-dialin-webhook endpoint uses a separate secret (daily_dialin_webhook_secret / VOXRAY_DAILY_DIALIN_WEBHOOK_SECRET) validated via the X-Webhook-Secret header, not the server API key.

Request and Response Format

All JSON endpoints consume and produce application/json. Clients may send X-Request-ID; the server echoes it in response meta.requestId. Body size limit: Request bodies are capped at max_request_body_bytes (default 256 KB). Configure via VOXRAY_MAX_BODY_BYTES.

Success envelope

{
  "data": { "<payload fields>" },
  "meta": { "requestId": "550e8400-e29b-41d4-a716-446655440000" }
}
Always read the response payload from body.data. The meta object is present on all successful responses.

Error envelope

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Missing or empty offer",
    "requestId": "550e8400-e29b-41d4-a716-446655440000",
    "details": [
      { "field": "offer", "message": "required" }
    ]
  }
}
details is included only for validation errors and contains per-field information.

HTTP status codes

StatusError CodeMeaning
200Success
201Resource created
204No content (PATCH ICE ack, metrics disabled)
400BAD_REQUESTInvalid JSON, malformed request, or method not allowed
401UNAUTHORIZEDMissing or invalid API key / webhook secret
404NOT_FOUNDRoute or session not found
422VALIDATION_ERRORRequired field missing or empty
500INTERNAL_ERRORUnrecovered server error
503SERVICE_UNAVAILABLEDependency unavailable (Redis, Opus encoder)

All error codes

CodeDescription
BAD_REQUESTMalformed or invalid request; also returned for 405 Method Not Allowed
UNAUTHORIZEDAPI key required or invalid
FORBIDDENReserved
VALIDATION_ERRORField validation failed; includes details array
NOT_FOUNDResource or route not found
CONFLICTReserved
SERVICE_UNAVAILABLERequired dependency unavailable
INTERNAL_ERRORInternal server error (panic recovered or unhandled)
RATE_LIMIT_EXCEEDEDReserved (rate limiting not implemented)
UNPROCESSABLE_ENTITYReserved (validation uses VALIDATION_ERROR)

Endpoints Overview

MethodPathAuthDescription
GET/healthNoLiveness probe
GET/readyNoReadiness probe; 503 if Redis unreachable
GET/metricsNoPrometheus metrics text; 204 if disabled
GET/swagger/NoSwagger UI
POST/webrtc/offerYes*WebRTC SDP offer/answer
POST/startYes*Create runner session
POST/sessions/{id}/api/offerYes*Session WebRTC SDP offer
PATCH/sessions/{id}/api/offerYes*ICE trickle acknowledgment
GET/wsYes*WebSocket upgrade for voice pipeline
POST/NoTelephony provider webhook
GET/telephony/wsNoTelephony media WebSocket
POST/daily-dialin-webhookSecret†Daily PSTN dial-in webhook
* When server_api_key is configured. † Uses X-Webhook-Secret when daily_dialin_webhook_secret is set.

Endpoint Details

GET /health

Liveness probe. Returns 200 when the server process is running regardless of dependency state.
curl http://localhost:3042/api/v1/health
{
  "data": { "status": "ok" },
  "meta": { "requestId": "550e8400-e29b-41d4-a716-446655440000" }
}

GET /ready

Readiness probe. When session_store is redis, pings Redis before responding. Use this endpoint for load balancer readiness checks.
curl http://localhost:3042/api/v1/ready
503 response (Redis unreachable):
{
  "error": {
    "code": "SERVICE_UNAVAILABLE",
    "message": "session store unavailable",
    "requestId": "..."
  }
}

GET /metrics

Returns Prometheus metrics in text exposition format. When metrics_enabled is false, returns 204 No Content.
curl http://localhost:3042/metrics
Restrict access to /metrics in production using a network policy or firewall rule. The endpoint has no built-in authentication.

GET /swagger/

Serves the Swagger UI. The OpenAPI definition file is docs/swagger.json. Append doc.json for the raw spec: GET /swagger/doc.json.

POST /webrtc/offer

Submit a WebRTC SDP offer. The server returns an SDP answer and wires the new transport into the voice pipeline. Available when transport is smallwebrtc or both. Request body:
FieldTypeRequiredDescription
offerstringYesSDP offer string (typically stringified RTCSessionDescription)
Example:
curl -X POST http://localhost:3042/api/v1/webrtc/offer \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-api-key" \
  -d '{"offer":"v=0\r\n..."}'
Success (200):
{
  "data": { "answer": "v=0\r\n..." },
  "meta": { "requestId": "550e8400-e29b-41d4-a716-446655440000" }
}
StatusCodeCondition
200SDP answer returned
401UNAUTHORIZEDMissing or invalid API key
422VALIDATION_ERRORoffer field missing or empty
503SERVICE_UNAVAILABLEOpus encoder unavailable
500INTERNAL_ERRORTransport start failed

POST /start

Create a runner session. Returns a sessionId plus optional ICE config or Daily room details. Supports idempotency via the Idempotency-Key header. Headers:
HeaderRequiredDescription
Idempotency-KeyNoOpaque string; same key returns cached 201 for 24 hours
Request body:
FieldTypeRequiredDescription
createDailyRoombooleanNoCreate a Daily.co room and return room URL and token
enableDefaultIceServersbooleanNoInclude default STUN server in iceConfig
bodyobjectNoArbitrary payload stored with the session; merged into request_data on session offer
Example:
curl -X POST http://localhost:3042/api/v1/start \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-api-key" \
  -H "Idempotency-Key: client-request-abc123" \
  -d '{"enableDefaultIceServers":true,"body":{"userId":"u-1"}}'
Success (201):
{
  "data": {
    "sessionId": "550e8400-e29b-41d4-a716-446655440000",
    "iceConfig": {
      "iceServers": [{ "urls": ["stun:stun.l.google.com:19302"] }]
    }
  },
  "meta": { "requestId": "660e8400-e29b-41d4-a716-446655440001" }
}
When createDailyRoom is true, data also contains dailyRoom (room URL) and dailyToken.
StatusCodeCondition
201Session created
401UNAUTHORIZEDMissing or invalid API key
500INTERNAL_ERRORSession store or Daily configuration error

POST /sessions//api/offer

Submit a WebRTC SDP offer for an existing session. The id path parameter must be a valid UUID obtained from POST /start. Path parameters:
ParameterTypeDescription
idUUID stringSession ID from POST /start
Request body:
FieldTypeRequiredDescription
sdpstringYesSDP offer string
typestringNoSDP type hint
pc_idstringNoPeer connection identifier
restart_pcbooleanNoForce peer connection restart
request_dataobjectNoOverrides session body for this request
requestDataobjectNoAlias for request_data
Example:
curl -X POST \
  http://localhost:3042/api/v1/sessions/550e8400-e29b-41d4-a716-446655440000/offer \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-api-key" \
  -d '{"sdp":"v=0\r\n..."}'
Success (200):
{
  "data": { "answer": "v=0\r\n...", "type": "answer" },
  "meta": { "requestId": "660e8400-e29b-41d4-a716-446655440001" }
}
StatusCodeCondition
200SDP answer returned
400BAD_REQUESTid is not a valid UUID
401UNAUTHORIZEDMissing or invalid API key
404NOT_FOUNDSession not found
422VALIDATION_ERRORsdp field missing or empty
503SERVICE_UNAVAILABLETransport HandleOffer failed

PATCH /sessions//api/offer

ICE trickle acknowledgment. Fire-and-forget — returns 204 with no body.
curl -X PATCH \
  http://localhost:3042/api/v1/sessions/550e8400-e29b-41d4-a716-446655440000/offer \
  -H "X-API-Key: your-api-key"
Returns 204 No Content on success. Same 400/401/404 errors as POST apply.

GET /ws

WebSocket upgrade for the real-time voice pipeline. One connection equals one session. The API key check (when configured) is performed before the protocol upgrade — a 401 JSON response is returned if it fails. Query parameters:
ParameterValueDescription
rtvi1Use RTVI protocol framing
formatprotobufBinary protobuf frames instead of JSON
Browser example:
const ws = new WebSocket(`ws://localhost:3042/ws`);
// To send the API key, use a backend proxy that injects the Authorization header,
// or pass it as a query parameter if your server build supports it.
Go example:
import "voxray-go/pkg/transport/websocket"

transport := websocket.NewClientTransport("ws://localhost:3042/ws", nil)
if err := transport.Start(ctx); err != nil {
    log.Fatal(err)
}
// Read/write frames via transport.Input() and transport.Output()

POST / (Telephony webhook)

Accepts webhook callbacks from Twilio, Telnyx, Plivo, and Exotel when runner_transport is set to one of those providers. Response format depends on the provider.
Returns 200 OK with Content-Type: application/xml:
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Connect>
    <Stream url="wss://your-host/telephony/ws"/>
  </Connect>
</Response>

GET /telephony/ws

WebSocket endpoint for telephony media streams. The provider is detected automatically from the first frames received. Available when runner_transport is twilio, telnyx, plivo, or exotel. No authentication required.

POST /daily-dialin-webhook

Handles incoming Daily PSTN dial-in events. Available when runner_transport=daily and dialin=true. When daily_dialin_webhook_secret is set, the request must include X-Webhook-Secret: <secret>. Request body:
FieldTypeRequiredDescription
testbooleanNoIf true, server replies OK without creating a room
FromstringYes (non-test)Caller number
TostringYes (non-test)Dialed number
callIdstringYes (non-test)Unique call identifier
callDomainstringYes (non-test)Daily call domain
Success (201, non-test):
{
  "data": {
    "dailyRoom": "https://example.daily.co/room-name",
    "dailyToken": "eyJ...",
    "sessionId": "550e8400-e29b-41d4-a716-446655440000"
  },
  "meta": { "requestId": "..." }
}

Middleware

Middleware is applied in the following order for every request:
OrderMiddlewareBehavior
1RecoveryWraps the entire mux; catches panics, logs error and stack trace, returns 500 INTERNAL_ERROR
2MetricsPer-route (when metrics_enabled is true); records request count, duration, HTTP status, and active connections in Prometheus
3CORSApplied inside handlers for /webrtc/offer, /start, and /sessions/; sets Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers: Content-Type, Authorization, X-API-Key
Auth is enforced per-handler via requireAPIKey, not as global middleware.

Idempotency

POST /start supports the Idempotency-Key request header. Submitting the same key within 24 hours returns the cached 201 Created response body without creating a new session. The cache is in-memory and does not survive process restarts.
POST /api/v1/start
Idempotency-Key: my-unique-client-key-123

Interactive API Docs

The full interactive Swagger UI is served at /swagger/ when the server is running. The raw OpenAPI 2.0 spec is at /swagger/doc.json and is also available as docs/swagger.json in the repository.