Stream an OpenAI Realtime API agent with a cXML script
<Stream>In this guide, we will build a Node.js application that serves a
cXML Script
that initiates a two-way (bidirectional)
<Stream>
to a Speech-to-Speech model on the OpenAI Realtime API.
When a caller initiates a call to the assigned phone number,
the SignalWire platform requests and runs the cXML script.
Prerequisites
Before you begin, ensure you have:
- SignalWire Space - Sign up free
- OpenAI API Key - Get access (requires paid account)
- Node.js 20+ - For running the TypeScript server (Install Node)
- ngrok or other tunneling service - For local development tunneling (Install ngrok)
- Docker (optional) - For containerized deployment
Quickstart
Clone and install
Clone the SignalWire Solutions repository, navigate to this example, and install.
git clone https://github.com/signalwire/cXML-realtime-agent-stream
cd cXML-realtime-agent-stream
npm install
Add OpenAI credentials
Select the Local or Docker tab below depending on where you plan to run the application.
- Local
- Docker
When running the server on your local machine, store your credentials in a .env file.
cp .env.example .env
Edit .env and add your OpenAI API key:
OPENAI_API_KEY=sk-your-actual-api-key-here
When running the server in production with the Docker container, store your credentials in a secrets folder.
mkdir secrets
echo "sk-your-actual-api-key-here" > secrets/openai_api_key.txt
Run application
- Local
- Docker
npm run build
npm start
docker-compose up --build signalwire-assistant
Your AI assistant webhook is now running at http://localhost:5050/incoming-call.
Make sure your server is running and the health check passes:
curl http://localhost:5050/health
# Should return: {"status":"healthy"}
Create a cXML script
Next, we need to tell SignalWire to request cXML from your server when a call comes in.
- Navigate to My Resources in your Dashboard.
- Click Create Resource, select Script as the resource type, and choose
cXML. - Under
Handle Using, selectExternal Url. - Set the
Primary Script URLto your server's webhook endpoint.
Select the Local tab below if you ran the application locally, and the Docker tab if you're running it with Docker.
- Local
- Docker
SignalWire must be able to reach your webhook from the internet. For local development, use ngrok or another tunneling service to expose your local server.
Use ngrok to expose port 5050 on your development machine:
ngrok http 5050
The output will look like:
Forwarding https://abc123def456.ngrok.io -> http://localhost:5050
Append /incoming-call to the HTTPS URL provided by ngrok:
https://abc123def456.ngrok.io/incoming-call
Use this as the Primary Script URL when creating your cXML script in the SignalWire Dashboard.
For production environments, set your server URL + /incoming-call:
https://your-domain.com/incoming-call
For this example, you must include /incoming-call at the end of your URL. This is the specific webhook endpoint that our application uses to handle incoming calls.
- Give the cXML Script a descriptive name, such as "AI Voice Assistant".
- Save your new Resource.
Assign phone number or SIP address
To test your AI assistant, create a SIP address or phone number and assign it as a handler for your cXML Script Resource.
- From the My Resources tab, select your cXML Script
- Open the Addresses & Phone Numbers tab
- Click Add, and select either SIP Address or Phone Number
- Fill out any required details, and save the configuration
Test application
Dial the SIP address or phone number assigned to your cXML Script. You should now be speaking to your newly created agent!
How it works
This section walks through the key components of the integration. Start with the system architecture to understand the full picture, then explore each component in detail.
Configuration
Environment variables
Set up your environment variables for different deployment scenarios:
- Local
- Docker
Create a .env file in your project root:
# Required
OPENAI_API_KEY=sk-your-actual-api-key-here
# Optional
PORT=5050
AUDIO_FORMAT=g711_ulaw # or 'pcm16' for HD audio
For production, store your API credentials securely using Docker secrets rather than environment variables. This keeps sensitive data out of version control and environment files.
Set up secrets:
mkdir -p secrets
echo "sk-your-actual-api-key-here" > secrets/openai_api_key.txt
docker-compose.yml configuration:
The docker-compose.yml file references the secret and mounts it into the container:
services:
signalwire-assistant:
# ... other config
secrets:
- openai_api_key
secrets:
openai_api_key:
file: ./secrets/openai_api_key.txt
Reading secrets in your application:
Your application reads from the Docker secret at runtime, checking the secret file first and falling back to an environment variable:
import * as fs from 'fs';
function getOpenAIApiKey(): string {
// First try to read from Docker secret (for containerized deployments)
const secretPath = '/run/secrets/openai_api_key';
try {
if (fs.existsSync(secretPath)) {
const apiKey = fs.readFileSync(secretPath, 'utf8').trim();
if (apiKey) {
return apiKey;
}
}
} catch (error) {
// Fall back to environment variable if secret reading fails
// (logging omitted for simplicity)
}
// Fallback to environment variable
const envApiKey = process.env.OPENAI_API_KEY;
if (envApiKey) {
return envApiKey;
}
return '';
}
const OPENAI_API_KEY = getOpenAIApiKey();
The actual implementation includes startup validation that checks:
- API Key: Throws an error if
OPENAI_API_KEYis missing, with helpful instructions for both local and Docker setups - Audio Format: Validates that
AUDIO_FORMATis eitherg711_ulaworpcm16, rejecting invalid values
This means configuration errors are caught immediately at startup, preventing runtime failures later. If you see configuration errors when starting the application, check the error message—it includes specific instructions for fixing the issue.
Important reminders:
- Always add
secrets/to your.gitignoreto prevent accidental commits - Docker secrets are mounted at
/run/secrets/inside the container - Keep credentials out of
.envfiles and version control
Audio codec
Choose the right audio codec for your use case. The default is G.711 μ-law.
PCM16 @ 24kHz
Crystal clear audio for demos and high-quality applications
Sample rate: 24 kHz
Bandwidth: ~384 kbps
Quality: High definition
G.711 μ-law @ 8kHz
Standard telephony quality, lower bandwidth usage
Sample rate: 8 kHz
Bandwidth: ~64 kbps
Quality: Standard telephony
The application automatically sets the correct codec in your cXML response based on the AUDIO_FORMAT environment variable. Just configure the environment variable:
# In your .env file
AUDIO_FORMAT=pcm16 # or g711_ulaw
The application will use pcm16 (24kHz HD audio) when set, or default to g711_ulaw (8kHz standard telephony) if not set.
Troubleshooting
Refer to this table if you encounter issues running the application.
| Issue | Cause | Solution |
|---|---|---|
| No audio from AI | Codec mismatch or incorrect codec configuration | • Check AUDIO_FORMAT environment variable• Verify SignalWire and application codec match |
| Invalid AUDIO_FORMAT error | Invalid environment variable value | • Verify AUDIO_FORMAT is either g711_ulaw or pcm16• Check for typos or extra whitespace • Remove the variable to use default ( g711_ulaw) |
| Server fails to start | Port 5050 already in use | • Check what's running on port 5050: lsof -i :5050• Stop the conflicting application or use a different port with PORT=5051 npm start |
| Health check failing | Server crashed or not responding | • Check server logs for error messages • Verify all configuration is correct • Try accessing /health endpoint directly in browser |
| Missing OPENAI_API_KEY | Configuration error | • Verify OPENAI_API_KEY in .env file (local)• Verify Docker secrets are configured (Docker) |
| Calls not connecting after ngrok restart | ngrok URL changed | • ngrok generates a new URL each time you restart • Update the webhook URL in SignalWire Dashboard with the new ngrok URL • Restart ngrok and update SignalWire before testing |
Resources
SignalWire + OpenAI Realtime
Production-ready implementation with all features
Complete working example with weather and time functions, error handling, and production Docker setup
OpenAI Realtime API Guide
Official documentation for the OpenAI Realtime API
cXML Reference
Complete reference for Compatibility XML
@openai/agents SDK Documentation
NPM package documentation for the OpenAI Agents SDK