How it connects
Prerequisites
- A Twilio account with Programmable Voice and one phone number (used as the caller id on the leg to OpenAI)
- An OpenAI account with a project that has Realtime API access
- Somewhere to run a Node 20+ server on a public HTTPS URL (Railway, Fly, Render, a VM)
- A Bluejay account
Part 1: OpenAI
1. Get the project id and an API key
Open platform.openai.com, pick the project, and go to Settings, then Project, then General. Copy the Project ID. It starts withproj_.
Create an API key in the same project under API keys. The key and the project id must belong to the same project. If they do not, OpenAI rejects the accept call with mismatched_project.
To confirm which project a key belongs to, make any POST request with it and read the openai-project response header:
2. Register the webhook
OpenAI announces each incoming SIP call by posting to a webhook. Without one, every INVITE tosip.api.openai.com fails.
Go to Settings, then Project, then Webhooks, and create an endpoint:
Copy the signing secret shown after you save. It starts with
whsec_ and is shown once. If the project already has an endpoint for realtime.call.incoming, edit that one. Two endpoints both try to accept the same call.
You now have OPENAI_PROJECT_ID, OPENAI_API_KEY, and OPENAI_WEBHOOK_SECRET.
Part 2: Twilio
Everything in this part is in the Twilio Console under Communications, then Voice, then SIP domains.
3. Create a credential list
Bluejay authenticates to your SIP domain with a username and password. Twilio stores those in a credential list.- Open the Credential lists tab and click Create Credential List.

- Credential list friendly name:
bluejay-sip-cred. - Under Add credentials, enter a Username of
bluejayand a Password of at least 12 characters with one uppercase letter, one lowercase letter, and one digit. - Click Create.

Do this with the REST API
Do this with the REST API
4. Create the SIP domain
- Open the SIP domains tab and click Create SIP Domain.

- SIP domain friendly name:
Twilio OpenAI. - SIP URI: pick a name, for example
twilio-openai. Twilio appends.sip.twilio.com. The name must be unique across all of Twilio. - Under Voice authentication, open Credential lists and select
bluejay-sip-cred. Leave IP access control lists empty. - Click Create.

- The domain’s details page opens. Under Call control configuration, click Edit configuration.
- Set Configure method to
Webhook, Webhook URL tohttps://<your-server>/twiml, and HTTP method toHTTP POST. Click Save.

- Leave SIP registration disabled.
Do this with the REST API
Do this with the REST API
The REST parameter for the webhook URL is named The mapping must be created under
VoiceUrl.Auth/Calls. The domain also has a top-level CredentialListMappings resource; that one controls SIP registration and does nothing for incoming calls.Part 3: The server
Three routes. Twilio calls/twiml when a call arrives and /dial-status when the leg to OpenAI ends. OpenAI calls /openai-webhook when its SIP endpoint receives the call, and your server accepts it with the agent’s instructions.
5. Create the project
6. Set the environment
7. Write the server
server.js
/openai-webhookusesexpress.raw(). The signature is computed over the exact request bytes, so the body must not be parsed before verification.- The route responds
200before calling accept. OpenAI drops the call if the webhook takes too long. callerIdon<Dial>is required. The incoming leg’s caller is a SIP URI, and Twilio needs a phone number to place the outbound leg.
8. Deploy
Deploy to any host with a public HTTPS URL and set the environment variables from step 6. Confirm the routes answer:Part 4: Bluejay
9. Create the agent
In Agent Settings, then Connection, set the connection type to SIP and fill in:
The user part of the URI (
openai here) can be any value.
Do this with the REST API
Do this with the REST API
10. Create and run a simulation
- Go to the Simulations page, click Create in the top right, select the agent, and add a digital human. An intent like “Your dishwasher is leaking. Book a repair for next Tuesday afternoon.” works well with the example agent.
- In the Simulations tab, click Run on the simulation, then click Run calls.
11. Check the result
Open the finished call. You should see a transcript where the agent greets the caller, collects the appliance and the day, and confirms a date that matches the calendar. Your server logs show the two requests it answered:Part 5: Going further, tool calls and traces
With the integration working, you can make each Bluejay result richer. Bluejay puts anX-Simulation-Result-Id header on every call it places. If your server carries that id through the call and posts back when the call ends, the result page shows the tool calls the agent made and an OpenTelemetry trace next to the transcript.
Three changes to the server from Part 3.
12. Carry the result id through the call
Twilio passes Bluejay’s SIP headers to/twiml as form fields prefixed with SipHeader_. Read SipHeader_X-Simulation-Result-Id and append it to the SIP URI. Twilio sends headers written after ? on the URI, and OpenAI returns them to your webhook in data.sip_headers.
/openai-webhook, read it before accepting:
13. Give the agent a tool
Tools are declared in the accept body. This example books a repair. ReplacerunTool with a call into your own system, and tell the model in INSTRUCTIONS that it must call the tool before confirming anything, or it will announce a booking it never made.
tools, tool_choice, and input transcription to the accept body, and start a session after a successful accept:
14. Run the call session and report to Bluejay
Install the extra packages:BLUEJAY_API_KEY to the environment.
After accept, attach to the call with a websocket. The model emits response.function_call_arguments.done when it wants a tool. Run the tool, send the result back as a function_call_output item, and ask for the next response. Transcripts arrive as conversation.item.input_audio_transcription.completed for the caller and response.output_audio_transcript.done for the agent.
Each call gets one root span. Tool calls and utterances become child spans. The exporter sends them to Bluejay’s OTLP endpoint with your API key.
trace_ids in the same body, so sending both together produces duplicates.
Complete server.js with Part 5 included
Complete server.js with Part 5 included
15. Check the result again
Run the simulation from step 10 again and open the call. Alongside the transcript you should now see:- A
book_repairentry under tool calls, with the parameters the model extracted and the confirmation yourrunToolreturned. - A trace with a
voice.callroot span, oneexecute_tool book_repairchild, andagent.speechandcustomer.speechspans for each turn.
Troubleshooting
For help, ask in our Slack community.