
Company News
Socket Joins New OpenJS Program to Fund Node.js Security Work
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.
@pyai/twilio
Advanced tools
One-line bridge from a Twilio Media Streams phone call to a PyAI Omni voice agent, handles mu-law/PCM16 transcode, resampling, barge-in, DTMF, and engine-native call control (transfer, hangup) for you.
@pyai/twilio bridges a live Twilio call to a PyAI Omni
voice agent. Point a Twilio number at a tiny server, hand the Media Streams
WebSocket to OmniAgent.bridge(...), and your caller is instantly talking to a
real listen → think → speak agent, with sub-500 ms turn-taking, natural
barge-in, DTMF, and live transfer-to-human. You write zero audio or DSP code.
import { OmniAgent } from "@pyai/twilio";
OmniAgent.bridge(twilioWebSocket, {
apiKey: process.env.PYAI_API_KEY!,
voice: "stock_emma_en_gb",
persona: "You are a warm, concise support agent for Acme.",
knowledge: async (q) => myVectorSearch(q), // optional per‑turn grounding
// sessionLabel: "support-line", // optional opaque tag echoed to your kb_endpoint
});
That single call is a complete phone agent.
Most voice stacks are a fragile chain of four vendors: speech-to-text → an LLM → text-to-speech → a telephony bridge, each with its own latency, billing, and failure mode. Omni collapses all of it into one realtime engine behind one WebSocket and one API key:
@pyai/twilio is the last mile: it makes Omni answer a real phone number.
🎁 Get $50 in free credit when you sign up
Create a key at console.pyai.com, email only, no credit card, no sales call. That's plenty of live Omni calls to build and test with, free. Your key works on every surface instantly.
The bridge owns the entire audio path so you never touch a codec or a resampler:
omniRate: 8000 to skip
resampling entirely.configure frame (voice, persona, optional knowledge endpoint).clear, so the agent stops mid‑word.knowledge(query) callback is invoked on each
finalized caller turn; whatever you return is pushed to the agent as grounding.transfer_to_human, end_call, send_dtmf, play_hold, collect. end_call
hangs up cleanly; pass twilioControl (your Twilio REST creds) and the bridge
also performs the transfer (and a guaranteed hangup) by callSid. Without
it, each verb surfaces via an on* callback for you to act on.npm install @pyai/twilio
Requires Node ≥ 22 (uses the ws WebSocket client; everything else is built‑in).
You'll need a PyAI key (pyai_live_… or a free pyai_test_… sandbox key), grab one with $50 free credit, and a Twilio
number with Media Streams.
When Twilio receives a call it fetches TwiML from your webhook. Use
<Connect><Stream> (not <Start><Stream>) so the socket is two‑way and the
agent can speak back:
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Connect>
<Stream url="wss://your-host.example.com/media" />
</Connect>
</Response>
connectStreamTwiML("wss://your-host/media") builds exactly this string for you.
Set the number's A call comes in webhook to https://your-host/voice.
import Fastify from "fastify";
import websocket from "@fastify/websocket";
import { OmniAgent, connectStreamTwiML } from "@pyai/twilio";
const app = Fastify();
await app.register(websocket);
app.post("/voice", (req, reply) =>
reply.type("text/xml").send(connectStreamTwiML(`wss://${req.headers.host}/media`)));
app.get("/media", { websocket: true }, (twilioWS) =>
OmniAgent.bridge(twilioWS, { apiKey: process.env.PYAI_API_KEY }));
await app.listen({ port: 8080, host: "0.0.0.0" });
Tunnel it (ngrok http 8080), point your Twilio number at https://<host>/voice,
and call the number. See examples/twilio-omni-voice-agent
for a complete, runnable version.
OmniAgent.bridge(twilioWS, options) → BridgeHandletwilioWS is the Twilio Media Streams socket (the Node ws socket your
framework hands you). Options:
| Option | Type | Notes |
|---|---|---|
apiKey | string | Required. pyai_live_… / pyai_test_…. Opaque, never parsed. |
sessionLabel | string | Optional opaque tag echoed to your kb_endpoint so you can branch per call. Nothing to pre-create; omit if unneeded. |
voice | string | Voice id (stock / clone / designed). |
persona | string | System prompt / role for the agent. |
knowledge | (q) => facts | Promise<facts> | Per‑turn grounding callback. |
kbEndpoint / kbToken | string | Customer‑hosted endpoint the engine pulls per turn. |
omniRate | 8000 | 16000 | 24000 | Omni session rate. Default 24000. 8000 = no resampling. |
baseURL | string | Defaults to https://api.pyai.com. |
twilioControl | { accountSid, authToken } | Optional Twilio REST creds so the bridge performs transfer_to_human + end_call by callSid. Omit to keep it audio-only and handle the verbs yourself. |
transferDestination | string | Fallback transfer target if an engine frame doesn't carry one. |
onTranscript / onTransfer / onError / onClose | callbacks | Observability + lifecycle. |
onSendDtmf / onPlayHold / onCollect / onEndCall | callbacks | Engine-native call-control verbs, surfaced for you to act on. |
Returns a handle with close() and the underlying omni client.
The bridge connects only to /v1/omni?format=pcm16&rate=…. sessionLabel is
the only connect-time tag; there is no model selector or agent-id alias.
The Omni side is strict native framing: server audio, transcript, and control
messages are binary 0x01/0x02/0x03 frames. Control JSON is keyed on
event; live 0x02 bodies are plain UTF-8 caller-text deltas.
parseOmniTranscriptBody normalizes those deltas and retains bounded direct-JSON
support for older bridges. The connect rate is caller input: 16/24 kHz
sessions receive 24 kHz agent audio, while 8 kHz sessions receive 8 kHz;
hello.audio_out is authoritative.
Pass your Twilio REST credentials and the agent's call-control decisions become real carrier actions, no extra code:
OmniAgent.bridge(twilioWS, {
apiKey: process.env.PYAI_API_KEY!,
persona: "You are Acme support. Escalate to a human on request.",
twilioControl: {
accountSid: process.env.TWILIO_ACCOUNT_SID!,
authToken: process.env.TWILIO_AUTH_TOKEN!,
},
transferDestination: "+15551230000", // fallback if the agent's config has none
});
When the agent calls transfer_to_human, the bridge redirects the live call to
the resolved destination; on end_call it hangs the call up. (transfer is a
blind redirect in this version; warm/announced transfer is on the roadmap.)
Also exported for custom pipelines (and fully unit‑tested):
import {
muLawEncode, muLawDecode, // G.711 companding
Resampler, makeResampler, // anti-aliased rational resampler
pcm16ToBytes, bytesToPcm16, // little-endian PCM16 <-> bytes
OmniClient, omniWsUrl, // raw Omni realtime client
parseTwilioMessage, twilioMedia, // Twilio frame parse/build
twilioClear, connectStreamTwiML,
dialTwiml, redirectCall, hangupCall, // Twilio REST call control
} from "@pyai/twilio";
src/omni.ts so they're trivial to update.twilioControl set, the bridge performs the
redirect for you via Twilio's REST API (using your Twilio credentials, not
PyAI's). Without it, the transfer event still surfaces via onTransfer(info)
so you can redirect the call yourself. The redirect is a blind transfer
(<Dial>); warm/announced transfer (a conference bridge) is on the roadmap.send_dtmf on Media Streams. Twilio's Media Streams can't inject outbound
DTMF without ending the stream, so the bridge surfaces send_dtmf via
onSendDtmf rather than performing it; handle it on a transport that supports
mid-call DTMF if you need it.streamSid, which is what Twilio's jitter buffer likes.Building your phone agent with an AI coding agent (Cursor, Claude Code, Codex)?
Add the PyAI MCP server (@pyai/mcp)
so it can mint a free key and explore PyAI directly:
// .cursor/mcp.json · or: claude mcp add pyai -- npx -y @pyai/mcp
{ "mcpServers": { "pyai": { "command": "npx", "args": ["-y", "@pyai/mcp"] } } }
The MCP tools cover key minting + REST (TTS/STT/voices); the Twilio↔Omni bridge
itself is this SDK. Full setup: the
mcp-quickstart
example.
npm install
npm test # node --test, mock Twilio + mock Omni sockets (no network)
npm run typecheck
npm run build # emits dist/
MIT licensed.
FAQs
One-line bridge from a Twilio Media Streams phone call to a PyAI Omni voice agent, handles mu-law/PCM16 transcode, resampling, barge-in, DTMF, and engine-native call control (transfer, hangup) for you.
The npm package @pyai/twilio receives a total of 7 weekly downloads. As such, @pyai/twilio popularity was classified as not popular.
We found that @pyai/twilio demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Company News
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.

Security News
Two compromised GitHub Actions were re-enabled with malicious tags intact, exposing thousands of downstream repositories to Mini Shai-Hulud.

Research
/Security News
A malicious Firefox extension fetches its payload after installation to evade detection, steal Google session cookies, and automate account takeover.