Connect to OpenAI

Connect a Callr Phone Number to an OpenAI Realtime AI Agent

📘

Overview

Calls 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

  1. A caller dials your Callr number.
  2. Your Callr Actions scenario uses dial@v2 to send the call to sip.api.openai.com over TLS.
  3. OpenAI sends a realtime.call.incoming webhook to your server.
  4. Your server accepts the call, with the model and the instructions of your agent.
  5. Audio flows between the caller and the agent until one side hangs up.
🚧

Your server must accept each call

OpenAI 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=tls

Step 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 API

OpenAI's newer Live API sends the same incoming call as a live.transport.incoming event, accepted with POST /v1/live/sessions/{session_id}/accept (the session settings go in a top-level session object). 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 first

Once 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 a response.create event. 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: 60

Replace 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.incoming event and a 200 from the accept request;
  • the Live Debugger shows dial@v2 with the status bridged.

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

SymptomLikely causeFix
The call never connectsYour server did not accept the callCheck that the webhook is enabled, that your endpoint is reachable over HTTPS, and that the accept request returns 200
Your server answers 400Signature check failedUse the signing secret of this webhook in OPENAI_WEBHOOK_SECRET
The call ends with 603 DeclineYour server rejected the callCheck your reject logic
The scenario refuses the SIP targetWrong URI formatUse sip:[email protected];transport=tls, with the proj_ prefix
Silence once connectedThe agent waits for the callerSpeak first, or send response.create (see above)

Did this page help you?