Executive Summary: Bridging Real-Time AI and Human Sales Execution

OpenAI’s Realtime API lets a voice agent handle first-line conversations such as qualification, discovery, objection handling and lead routing. Sooner or later a prospect asks for a person, or says something that signals buying intent. At that point the call has to move to a human Sales Development Representative (SDR) without losing context and with as little dead air as possible.

This playbook covers that handoff using the current OpenAI Realtime API, and it corrects a common misconception. OpenAI’s Realtime session is not a call you can re-point at a human. When a browser connects over WebRTC, it forms a peer connection with OpenAI. When a phone caller connects over SIP, the call terminates at OpenAI’s SIP endpoint. The API’s only built-in transfer primitive is a SIP `REFER` for SIP calls. For browser sessions, the handoff is an orchestrated reconnection through your own media layer.

A note on models. The guidance here targets `gpt-realtime-2.1` and `gpt-realtime-2.1-mini`, which OpenAI released on July 6, 2026. They update GPT-Realtime-2 (released May 7, 2026), which added configurable reasoning effort to speech-to-speech agents. The older `gpt-4o-realtime-preview` models are still listed in the SDK, but new builds should use the current generation. OpenAI’s changelog is the place to check for anything newer.

1. What You’re Building On (Current State)

ItemCurrent state
Latest models`gpt-realtime-2.1`, `gpt-realtime-2.1-mini` (July 6, 2026)
LatencyOpenAI reported at least a 25% p95 latency reduction across Realtime voice models through improved caching (July 2026)
TransportsWebRTC (recommended for browsers and mobile), WebSocket (typically server-side) and SIP (phone calls)
Session limit60 minutes maximum per Realtime session
Recommended voices`marin` and `cedar` for best quality
ReasoningConfigurable effort. OpenAI suggests starting at `low` for most production voice agents and raising it only for harder tasks

Pricing is per million tokens, not per minute. The figures below are from OpenAI’s announcement:

ModelAudio inputCached audio inputAudio output
`gpt-realtime-2.1`$32.00$0.40$64.00
`gpt-realtime-2.1-mini`$10.00$0.30$20.00

Don’t rely on a fixed sub-300ms latency figure from vendor marketing or older blog posts. Measure time-to-first-audio and p95 yourself, from your own users’ networks.

2. Transport Reality: WebRTC vs. SIP

WebRTC is the right transport when a prospect talks to your agent from a website or app. The browser is a peer of the Realtime API, which streams the model’s audio back as a remote media track. Events travel over a data channel.

SIP is the right transport when the prospect phones a number. You point a SIP trunk at OpenAI’s SIP endpoint (`sip:$PROJECT_ID@sip.api.openai.com;transport=tls`). Carriers such as Twilio, Telnyx and Bandwidth have published integration guides for this. Telnyx’s guide notes that TLS protects only signaling, so you also need SRTP for call audio. Azure OpenAI offers the same SIP flow in the Sweden Central and East US 2 regions.

Two consequences follow for human handoff:

1. SIP calls can be transferred by calling `POST /v1/realtime/calls/{call_id}/refer` with a `target_uri` such as `tel:+14155550123` or `sip:agent@example.com`. OpenAI relays a SIP `REFER` to your provider and returns `200 OK`. The downstream system then handles the rest of the call flow. This is a blind transfer, so the AI leg is gone once it succeeds.

2. WebRTC sessions have no equivalent transfer verb. The `hangup` endpoint ends either type of session, but it ends the call rather than moving it. To get a human on the line, the prospect’s audio has to reach the SDR over a path you control.

3. Three Handoff Architectures

```

Pattern 1: SFU in the middle (best for web-first, warm handoffs)

[Prospect browser] ◄──WebRTC──► [SFU room] ◄──► [AI agent participant ⇄ OpenAI Realtime]

▲

└── SDR joins the same room; AI is muted or leaves

Pattern 2: Direct WebRTC to OpenAI, then reconnect (simplest to start)

[Prospect browser] ◄──WebRTC──► [OpenAI Realtime]

│ on SDR claim: hang up OpenAI call

└──────new WebRTC connection────────► [SDR softphone / SFU / CCaaS]

Pattern 3: Phone call over SIP (REFER)

[PSTN] ► [SIP trunk] ► [OpenAI SIP endpoint] ──REFER──► [SDR's SIP URI / queue]

```

Pattern 1: SFU in the middle. The prospect joins a room on a media server such as LiveKit. An AI agent joins as a participant and talks to OpenAI Realtime on the backend. When a human is needed, the SDR joins the same room and the AI goes quiet or leaves. The prospect’s connection never changes, so there is no reconnection gap. LiveKit Agents has an OpenAI Realtime integration and a Python beta `WarmTransferTask`. That task puts the caller on hold with hold music, dials the human over SIP, gives them conversation context, and then merges the calls.

Pattern 2: Direct connection, then reconnect. This is the quickest pattern to ship because the browser connects straight to OpenAI. On claim, your server ends the OpenAI call and the browser opens a new connection to wherever your SDRs live. Expect a short audio gap while the new connection negotiates. The page can reuse its existing microphone track, so the prospect isn’t prompted again.

Pattern 3: SIP with REFER. For phone traffic, claim the SDR first, then REFER the caller to that SDR’s endpoint. Because REFER is blind, only transfer after an SDR has actually accepted. Otherwise the caller lands on a dead extension with no AI left to catch them.

4. Session Setup: Server-Brokered WebRTC with a Sideband Channel

OpenAI’s GA interface uses `POST /v1/realtime/client_secrets` for ephemeral keys and `POST /v1/realtime/calls` for SDP exchange. The older `/v1/realtime/sessions` flow and `gpt-4o-realtime-preview` are the legacy approach.

For handoffs, prefer the unified interface. Your server receives the browser’s SDP offer, forwards it with the session configuration, and reads the call ID from the `Location` header of the response. That call ID lets your server open a second “sideband” WebSocket to the same session, to watch events, run tools and update instructions. Keeping tool logic on the server also keeps your business rules private.

```

import express from "express";

import WebSocket from "ws";

const app = express();

app.use(express.static("public"));

const sessionConfig = {

type: "realtime",

model: "gpt-realtime-2.1",

audio: { output: { voice: "marin" } },

instructions:

"You are a friendly sales development assistant. Qualify the lead. " +

"If the caller asks for a human or shows strong buying intent, call escalate_to_human_sdr.",

tools: [

{

type: "function",

name: "escalate_to_human_sdr",

description: "Transfer this conversation to a live human sales representative.",

parameters: {

type: "object",

properties: {

summary: { type: "string", description: "Pain points, budget, timeline, qualification status." },

urgency: { type: "string", enum: ["high", "medium", "low"] },

},

required: ["summary", "urgency"],

},

},

],

};

// Browser POSTs its SDP offer (Content-Type: application/sdp)

app.post("/api/session", express.text({ type: "application/sdp" }), async (req, res) => {

try {

const form = new FormData();

form.set("sdp", req.body);

form.set("session", JSON.stringify(sessionConfig));

const r = await fetch("https://api.openai.com/v1/realtime/calls", {

method: "POST",

headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },

body: form,

});

if (!r.ok) return res.status(502).send("Failed to create realtime call");

const callId = r.headers.get("Location")?.split("/").pop();

attachSideband(callId);

res.type("application/sdp").send(await r.text()); // answer SDP back to the browser

} catch {

res.status(500).send("Failed to initialize realtime session");

}

});

function attachSideband(callId) {

const ws = new WebSocket(`wss://api.openai.com/v1/realtime?call_id=${callId}`, {

headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },

});

ws.on("message", async (raw) => {

const event = JSON.parse(raw.toString());

if (event.type !== "response.done") return;

for (const item of event.response.output ?? []) {

if (item.type === "function_call" && item.name === "escalate_to_human_sdr") {

const args = JSON.parse(item.arguments);

// 1. Close the tool call

ws.send(JSON.stringify({

type: "conversation.item.create",

item: { type: "function_call_output", call_id: item.call_id, output: JSON.stringify({ status: "queued" }) },

}));

// 2. Have the model speak the bridge line (speech-to-speech, no separate TTS step)

ws.send(JSON.stringify({

type: "response.create",

response: { instructions: "Tell the caller you're connecting them to a specialist and ask them to hold for a moment." },

}));

// 3. Start the push-to-claim workflow

startHandoff(callId, args);

}

}

});

}

app.listen(3000);

```

Two practical notes. First, reading tool calls from `response.done` avoids sending a new `response.create` while a response is still active. Second, developers have reported 404 `call_id_not_found` errors when opening a sideband connection for a call created with an ephemeral key, while calls created with a standard API key worked. Test your exact setup and prefer the server-brokered flow shown above for handoff-critical paths.

5. Detecting the Handoff Moment

Function calling is the cleanest trigger. The model decides when to call `escalate_to_human_sdr`, either because the prospect asked for a person or because your instructions define a threshold such as budget, timeline and decision-maker status. Combine it with deterministic rules on your side, for example keyword fallbacks, so a missed tool call doesn’t strand a hot lead. GPT-Realtime-2 and later also support parallel tool calls and audible preambles, which help the agent keep talking while a lookup or transfer is in progress.

Tool names must match exactly between your instructions, the tool schema and your event handler.

6. Push-to-Claim: Getting the Right SDR Quickly

Push-to-claim is a broadcast-and-lock pattern:

```

[Tool call fires] ─► [Server emits `sdr:incoming_lead` over WebSocket]

│

┌─────────────────────┼─────────────────────┐

▼ ▼ ▼

[SDR dashboard 1] [SDR dashboard 2] [SDR dashboard 3]

alert + Claim alert + Claim alert + Claim

```

1. Keep the AI engaged. The Realtime session stays open while SDRs are alerted. The model’s bridge line buys a few seconds. Don’t cancel the session or go silent.

2. Broadcast a lead card to available SDRs (Socket.io, Pusher or similar) containing the session or call ID, urgency and the AI’s summary.

3. Claim atomically. The first SDR to click sends `POST /api/sdr/claim`. Lock with a single Redis command that sets the key only if absent, with an expiry, for example `SET claim:{sessionId} {sdrId} NX EX 60`. Use this instead of a bare `SETNX`, which can’t set a TTL, so a crashed server could leave a lead locked forever. Everyone else gets a “already claimed” response.

4. Connect media using whichever pattern from section 3 you chose.

Example alert payload:

```

{

"event": "sdr:incoming_lead",

"sessionId": "call_or_session_id",

"company": "Acme Corp",

"summary": "Interested in enterprise tier, wants API integration details, timeline next quarter.",

"urgency": "high"

}

```

Only include fields you actually have. Don’t show a prospect name or company the AI didn’t collect.

7. Bridging the Media Path

Web-first: browser to SDR

  • Pattern 1 (SFU): Issue the SDR a token for the existing room. They join, you mute or remove the AI participant, and audio continues without renegotiation.
  • Pattern 2 (reconnect): After the claim, call `POST /v1/realtime/calls/{call_id}/hangup`, then have the browser connect to your softphone or SFU and ring the claiming SDR.

Telephony: phone call to SDR

  • Blind transfer: After claim, call `POST /v1/realtime/calls/{call_id}/refer` with the SDR’s `sip:` or `tel:` URI. Time it after the bridge line has finished playing, because the AI leg ends once the transfer is relayed.
  • Warm transfer: If the SDR should hear a briefing before the caller is connected, use a platform that supports it, such as LiveKit’s warm transfer workflow or your CCaaS vendor’s consult-transfer. LiveKit’s version dials the human over SIP and plays hold music to the caller. Hold music is generated by your platform, not by the Realtime model.
  • Carrier and codec checks: Confirm SRTP, TLS, and a codec both sides support. Misconfigured carriers have caused one-way or silent audio for others.

8. Passing the Baton: Context and CRM Sync

The SDR should see context at the moment they accept, not after. Build the handoff payload from:

  • The model’s `summary` and `urgency` arguments from the tool call.
  • Transcripts. Enable input transcription in the session configuration and capture transcript events as the call progresses, so you aren’t reconstructing the conversation afterward.
  • Metadata you already have: page URL, UTM parameters, CRM record ID, and for SIP calls the `From` and `Call-ID` headers delivered in the `realtime.call.incoming` webhook.

For phone transfers, key context to the caller’s number or SIP Call-ID in your own store, so the SDR’s screen can load it when the call arrives.

Log asynchronously to HubSpot, Salesforce or your CRM: transcript, summary, duration, outcome and a clear stage such as “AI-qualified, human handoff.” Don’t block the media path on CRM writes.

9. Edge Cases and Production Practices

  • No SDR claims in time. Set a timeout you can tune, for example 15 to 30 seconds. On expiry, have the still-connected AI apologize, then offer to book a meeting or take a message. This only works if you kept the Realtime session alive, which is another reason not to hang up early.
  • Claimed but never connected. If the SDR claims and then fails to connect, release the lock and re-broadcast, or fall back to the AI.
  • Session limits. A Realtime session lasts at most 60 minutes. Long waits and long conversations should not be allowed to run into that limit.
  • Network drops during handoff. Keep the room or call leg alive briefly, and let the prospect rejoin to the same SDR where your platform supports it.
  • Orphaned sessions and cost. OpenAI prices Realtime by tokens, not by connection time, but context is carried into each response, so long or idle sessions are not free. Cached input is cheaper, so keep your instructions stable at the start of the context. After handoff, make sure the OpenAI leg is ended, via `hangup` for WebRTC and SIP or implicitly by REFER.
  • Model selection. `gpt-realtime-2.1-mini` costs far less. Developers have reported tool-calling differences between mini versions, including one report of the 2.1 mini not calling tools in SIP sessions. Regression-test your handoff tool on whichever model you ship.
  • Security. Verify webhook signatures on `realtime.call.incoming`, keep API keys server-side, and set `OpenAI-Safety-Identifier` when minting ephemeral keys.
  • Legal and trust. Disclose that the caller is speaking with an AI, and check call-recording and consent rules for every jurisdiction you call into or accept calls from.

Conclusion

A reliable AI-to-human handoff depends on treating the transfer as an orchestration problem rather than a feature of the voice model. Use a server-brokered WebRTC session with a sideband channel for tool handling. Choose a media architecture up front: an SFU for seamless web handoffs, reconnect for simplicity, or SIP `REFER` for phone traffic. Use an atomic push-to-claim queue so exactly one SDR gets the lead, and deliver context to that SDR when they accept. Keep the AI on the line until a human is confirmed, and you avoid both the dead air and the dropped prospect.

References

  • OpenAI, Realtime API with WebRTC: <https://developers.openai.com/api/docs/guides/realtime-webrtc>
  • OpenAI, Realtime API with SIP: <https://developers.openai.com/api/docs/guides/realtime-sip>
  • OpenAI, Webhooks and server-side controls: <https://developers.openai.com/api/docs/guides/realtime-server-controls>
  • OpenAI, Realtime conversations: <https://developers.openai.com/api/docs/guides/realtime-conversations>
  • OpenAI API changelog: <https://developers.openai.com/api/docs/changelog>
  • OpenAI Developer Community, gpt-realtime-2.1 announcement and pricing: <https://community.openai.com/t/new-realtime-models-on-the-api-gpt-realtime-2-1-and-gpt-realtime-2-1-mini/1385896>
  • Microsoft Learn, GPT Realtime API via SIP (Azure): <https://learn.microsoft.com/en-us/azure/foundry/openai/how-to/realtime-audio-sip>
  • LiveKit, Agent-assisted warm transfer: <https://docs.livekit.io/telephony/features/transfers/warm>
  • Bandwidth, OpenAI Realtime SIP integration: <https://dev.bandwidth.com/docs/voice/integrations/openai/realtime/sip>