Connect to OpenAI
Connect a Callr Phone Number to an OpenAI Realtime AI Agent
OverviewCalls to your Callr number are routed over SIP to an OpenAI Realtime voice agent. Callr handles the phone number, carrier connectivity and call routing. OpenAI handles speech recognition, reasoning, tools and speech synthesis.
How it works
- A caller dials your Callr number.
- Your Callr Actions scenario uses
dial@v2to send the call tosip.api.openai.comover TLS. - OpenAI sends a
realtime.call.incomingwebhook to your server. - Your server accepts the call, with the model and the instructions of your agent.
- Audio flows between the caller and the agent until one side hangs up.
Your server must accept each callOpenAI does not answer SIP calls on its own. Without a server that accepts the call when the webhook arrives, the call never connects.
Prerequisites
- A Callr account, a Callr phone number and a Callr Actions scenario
- An OpenAI API key, and a project with access to the Realtime API
- A public HTTPS endpoint for the webhook (ngrok works for testing)
Step 1 – Find your OpenAI project ID
In platform.openai.com, open Settings > Project > General and copy the project ID. It starts with proj_. Your SIP URI is:
sip:[email protected];transport=tlsStep 2 – Create the webhook
In Settings > Project > Webhooks, create a webhook:
- URL: your server's endpoint, for example
https://example.com/openai/webhook - Event:
realtime.call.incoming
Copy the webhook signing secret: your server uses it to check that requests come from OpenAI.
Live APIOpenAI's newer Live API sends the same incoming call as a
live.transport.incomingevent, accepted withPOST /v1/live/sessions/{session_id}/accept(the session settings go in a top-levelsessionobject). A pending call can trigger both events: the first accept wins. This guide uses the Realtime endpoints. See OpenAI's SIP guide.
Step 3 – Accept calls on your server
Each incoming call sends a realtime.call.incoming event with a call_id. Your server checks the signature, then accepts the call with POST /v1/realtime/calls/{call_id}/accept, passing the model and the instructions of your agent.
import os
import requests
from flask import Flask, Response, request
from openai import InvalidWebhookSignatureError, OpenAI
app = Flask(__name__)
client = OpenAI() # reads OPENAI_API_KEY and OPENAI_WEBHOOK_SECRET
INSTRUCTIONS = "You are the phone assistant of Example Corp. Keep your answers short."
@app.post("/openai/webhook")
def openai_webhook():
try:
event = client.webhooks.unwrap(request.data, request.headers)
except InvalidWebhookSignatureError:
return Response("invalid signature", status=400)
if event.type == "realtime.call.incoming":
call_id = event.data.call_id
resp = requests.post(
f"https://api.openai.com/v1/realtime/calls/{call_id}/accept",
headers={"Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}"},
json={
"type": "realtime",
"model": "gpt-realtime-2",
"instructions": INSTRUCTIONS,
},
timeout=10,
)
app.logger.info("accept %s -> %s", call_id, resp.status_code)
return Response(status=200)
if __name__ == "__main__":
app.run(port=8000)Set OPENAI_API_KEY and OPENAI_WEBHOOK_SECRET before you start the server. The event's data.sip_headers holds the SIP From, To and Call-ID headers. The caller's number is in From, because dial@v2 keeps the original caller ID by default.
To refuse a call, call POST /v1/realtime/calls/{call_id}/reject instead, optionally with a status_code such as 486 (busy). Without one, OpenAI answers 603 Decline.
Make the agent speak firstOnce the call is accepted, the agent waits for the caller to speak. To have it greet the caller, open a WebSocket to
wss://api.openai.com/v1/realtime?call_id={call_id}and send aresponse.createevent. See OpenAI's Realtime API with SIP guide.
Step 4 – Create the Callr scenario
description: Route inbound calls to an OpenAI Realtime agent
compat:
version: "2025-09-01"
defaults:
autoAnswer: false
branches:
inbound-call:
actions:
- action: dial@v2
params:
targets:
- number: sip:[email protected];transport=tls
timeout: 60Replace proj_XXXXXXXXXXXXXXXX with your project ID. transport=tls encrypts the SIP signaling.
Step 5 – Attach the scenario to your number
In the Callr Console, open your phone number and attach the scenario.
Step 6 – Test
Call your Callr number, then check that:
- your server logs the
realtime.call.incomingevent and a200from the accept request; - the Live Debugger shows
dial@v2with the statusbridged.
Add a fallback
Routing stays in Callr, so you can add business hours, an IVR before the agent, recording, or a fallback to a human when the agent does not answer:
compat:
version: "2025-09-01"
branches:
inbound-call:
actions:
- action: startRecording@v2
- action: dial@v2
params:
targets:
- number: sip:[email protected];transport=tls
timeout: 20
# called if OpenAI does not answer within 20 seconds
- number: "+33199001234"
timeout: 60
result: $dial
- if: "!$dial.ok"
action: play@v3
params:
tts:
text: Sorry, we are unable to connect you at the moment.Call recording must comply with the rules of your jurisdiction.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The call never connects | Your server did not accept the call | Check that the webhook is enabled, that your endpoint is reachable over HTTPS, and that the accept request returns 200 |
Your server answers 400 | Signature check failed | Use the signing secret of this webhook in OPENAI_WEBHOOK_SECRET |
The call ends with 603 Decline | Your server rejected the call | Check your reject logic |
| The scenario refuses the SIP target | Wrong URI format | Use sip:[email protected];transport=tls, with the proj_ prefix |
| Silence once connected | The agent waits for the caller | Speak first, or send response.create (see above) |
Updated 5 days ago
