- An AssemblyAI API key
- An
agent_id. Don’t have one? Create an agent first. - A Bluejay account. Don’t have one? Sign up at getbluejay.ai.
- Python 3.12+
1. Clone and configure
.env. The agent id is in the agent’s URL in the dashboard:
.env
2. Call it yourself first
call.py speaks CHIRP the way Bluejay does, so a call that sounds right through it will sound right in a simulation. It records the agent to out/agent.wav.
--delay so the question starts while the greeting is still playing, and you have tested barge-in.
3. Host the bridge
Bluejay dials in, so the bridge needs a publicwss:// address. Any host that runs Python, terminates TLS, and passes WebSockets through will do. Before hosting, set the Basic-auth credentials Bluejay will send:
.env
- Railway
- Render
- Anywhere else
Use the Railway template, or from an existing project: New → GitHub Repo → the bridge repo. Railway reads
.python-version and requirements.txt to build, and the Procfile for the start command.Under Variables set ASSEMBLYAI_API_KEY, AGENT_ID, CHIRP_USER, and CHIRP_PASS; PORT arrives on its own. Then under Settings → Networking generate a domain, and give Bluejay wss://<that domain>/voice.- Don’t run it on an instance that sleeps when idle. Simulations arrive in bursts after a long quiet period, which is exactly when a scaled-to-zero instance is cold. The first calls of the run time out on the WebSocket upgrade, and Bluejay records them as
INCOMPLETED. - Concurrent simulations need concurrent sessions. Each call is one Voice Agent session on your account. Past the limit AssemblyAI returns
concurrency_exceeded, which the bridge passes to Bluejay as a failed call.
4. Point Bluejay at it
In Bluejay, create an Agent with connection type Websocket, URLwss://<your-host>/voice, and the same CHIRP_USER and CHIRP_PASS you set on the host. Run a simulation.
After a simulation
Bluejay has the transcript, the recording, and the evaluations. AssemblyAI has its own record of the same call, with a stereo recording, per-turn timings, and which replies were cut off:BLUEJAY_API_KEY set, that id is also written onto the Bluejay simulation result, so the two records point at each other. See Recordings and transcripts.
Configuration
A
.env next to bridge.py is loaded on startup, and real environment variables win over it.
How the two protocols meet
Two details are worth knowing, because both are the difference between a simulation that measures your agent and one that measures the bridge:
- Reply audio is paced. The API sends a reply faster than real time. Forwarding it straight through would put seconds of speech in Bluejay’s playback buffer, and an interruption would arrive to find the agent already committed to talking. The bridge stays at most 200 ms ahead, so
reply.donewithstatus: "interrupted"actually stops the voice. - Hanging up ends the session. When Bluejay closes the call the bridge sends
session.endand waits forsession.ended. Dropping the socket instead leaves the session resumable, and billable, for another 30 seconds, which over a suite of simulations is real money.
Cost
Every simulated call is a Voice Agent session billed to the API key in your.env, and Bluejay bills its own side. Running a suite costs real money, so keep CHIRP_USER and CHIRP_PASS set on anything hosted: without them, anyone who finds the URL can start sessions on your key.