curl --request GET \
--url https://api.pyai.com/v1/omni \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.pyai.com/v1/omni"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.pyai.com/v1/omni', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.pyai.com/v1/omni",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.pyai.com/v1/omni"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.pyai.com/v1/omni")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.pyai.com/v1/omni")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"error": {
"message": "<string>",
"type": "<string>",
"code": "invalid_request_error",
"param": "<string>"
}
}{
"error": {
"message": "<string>",
"type": "<string>",
"code": "invalid_request_error",
"param": "<string>"
}
}{
"error": {
"message": "<string>",
"type": "<string>",
"code": "invalid_request_error",
"param": "<string>"
}
}Open an Omni voice-agent session (WebSocket)
This is the primary way to build an AI voice agent on PyAI. Open this WebSocket, send one configure frame (voice, persona, knowledge endpoint, tools), then stream PCM16 audio both ways. There is nothing to create first: the session is authorized by your key’s org, and the whole agent travels in the configure frame.
Connect with wss://api.pyai.com/v1/omni?format=pcm16&rate=24000 (rate=16000 or rate=8000 for telephony; use rate=8000 for an 8 kHz G.711/Twilio leg so the only conversion is μ-law companding, no resampling, see the Telephony audio reference). Every binary frame starts with a one-byte kind: send audio as 0x01 || pcm16_bytes and control JSON as 0x03 || utf8_json. Server audio uses the same 0x01 prefix. Untagged PCM is not audio and is ignored.
Requires the omni:session scope (or the omni:* wildcard). The optional session_label is an opaque tag echoed to your own knowledge endpoint so you can branch per session; any value in your org’s namespace is accepted (PyAI stores no per-agent state).
Auth: send the key as the subprotocol, after the pyai.v1 marker: Sec-WebSocket-Protocol: pyai.v1, pyai-key.<API_KEY> (browsers can’t set headers on a WebSocket; server clients may use ?api_key= instead). PyAI echoes only the marker, so your key is never reflected in the 101. Browser apps: if the client can’t hold your secret key, mint a short-lived origin-locked token with POST /v1/omni/sessions first and use that token as the subprotocol.
See the Omni wire protocol reference for the frame catalog and close codes. The configured acknowledgement reports voice_instruct_supported for the selected synthesis path. Current Omni routes support neutral delivery only and report false. Unsupported custom voice_instruct values return unsupported_voice_instruct before applying configure. Omit the field to retain the session value, or send an empty string to reset a saved custom instruction to neutral. A missing capability field on an older engine means unknown. A voice_capabilities event reports capability changes after synthesis fallback.
Function calling transports: hosted catalog tools (enable by name), server tools registered with POST /v1/tools and bound to the agent, or client-loop tools on this socket (tool_call / tool_result). An inline endpoint or webhook_url on configure.tools[] is rejected with {event:"error", code:"unsupported_tool_transport"} and the configure is not applied.
curl --request GET \
--url https://api.pyai.com/v1/omni \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.pyai.com/v1/omni"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.pyai.com/v1/omni', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.pyai.com/v1/omni",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.pyai.com/v1/omni"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.pyai.com/v1/omni")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.pyai.com/v1/omni")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"error": {
"message": "<string>",
"type": "<string>",
"code": "invalid_request_error",
"param": "<string>"
}
}{
"error": {
"message": "<string>",
"type": "<string>",
"code": "invalid_request_error",
"param": "<string>"
}
}{
"error": {
"message": "<string>",
"type": "<string>",
"code": "invalid_request_error",
"param": "<string>"
}
}Authorizations
Use Authorization: Bearer pyai_live_... (or pyai_test_...).
Query Parameters
Optional opaque tag for this session, echoed to your own kb_endpoint so you can branch per session. The session is authorized by your key's org (PyAI stores no per-agent state); any value in your org's namespace is accepted. Must be safe as a header value (no control chars, ≤256 chars). Omit it entirely if you don't need per-session correlation.
Audio sample format for both directions.
pcm16 Caller-input sample rate in Hz. Use 24000 for browser/WebRTC, 16000 for wideband input, or 8000 for telephony. Agent output is 24 kHz for both 24 kHz and 16 kHz input sessions; 8 kHz sessions receive 8 kHz output. Read hello.audio_out, which is authoritative.
8000, 16000, 24000 Intentional server-side WebSocket auth option. Prefer the pyai.v1, pyai-key.<API_KEY> subprotocol pair in browsers so the key is not placed in a URL.
512Response
Switching Protocols, the Omni WebSocket is open.