사용할 API를 선택하면 연결 절차와 세션 이벤트를 확인할 수 있습니다.
브라우저를 GPT-Live에 연결하기
브라우저, 모바일, 서버 애플리케이션에서 GPT-Live와 음성을 주고받을 때는 WebRTC를 권장합니다. 이 가이드에서는 브라우저를 예로 들어 연결 방법을 설명합니다. 마이크 입력과 모델이 생성한 음성은 연결 협상 과정에서 설정한 미디어 트랙으로 전송됩니다. 전사, 세션 업데이트, 위임된 작업에 관한 JSON 이벤트는 데이터 채널로 전송됩니다.
브라우저가 SDP(Session Description Protocol) 오퍼를 생성합니다. 애플리케이션 서버는 프로젝트 API 키로 POST /v1/live/sessions를 호출해 오퍼를 보내고 SDP 응답을 받습니다. API 키와 세션 설정은 신뢰할 수 있는 서버에 보관하세요.
시작하기 전에
다음 항목을 준비하세요.
- GPT-Live에 액세스할 수 있는 프로젝트 API 키
- 선택한 SDK 예제를 실행할 서버 런타임. Node.js 예제에는 Node.js 22.6 이상이 필요합니다.
- HTTPS 또는 로컬호스트에서 페이지를 열고 해당 페이지에 마이크 접근 권한을 부여할 수 있는 브라우저
이 예제는 gpt-5.6-terra와 호스팅형 웹 검색을 사용해 Responses에 작업을 위임합니다. 백엔드 지침과 애플리케이션 도구는 위임 및 도구를 참고하세요. 음성과 백엔드 사용량은 비용 최적화를 참고하세요.
연결 절차 이해하기
- 사용자 동작에 응답해 마이크 접근 권한을 요청하고 마이크 트랙을 피어 연결에 추가하세요.
- SDP 오퍼를 생성하기 전에 데이터 채널을 만들고 이벤트 리스너를 등록하세요.
- 로컬 설명을 설정하고 ICE 후보 수집이 끝날 때까지 기다린 다음 오퍼를 서버로 보내세요.
- 서버에서
session과transport: { type: "webrtc", sdp: ... }가 포함된 JSON을 HTTP POST로 OpenAI에 보내세요. - 반환된 SDP 응답을 원격 설명으로 설정하세요. 데이터 채널에서
session.started를 받은 후 애플리케이션 명령어를 보내세요.
HTTP 요청으로 세션이 시작됩니다. 데이터 채널로 session.start를 보내지 마세요. 예제의 oai-events 문자열은 데이터 채널의 레이블입니다.
POST /v1/live/sessions로 WebRTC 세션을 만들면 초기화 시 음성 사용 시간 15초에 해당하는 요금이 청구됩니다. 세션이 실행되면 이 금액은 사용 시간에 따른 요금에서 차감됩니다. 실행 시간에 15초가 추가되는 것은 아닙니다. 비용 산정 방식은 WebRTC 초기화 요금을 참고하세요.
애플리케이션 서버 만들기
새 디렉터리에 서버 예제를 저장하고 서버 환경에 OPENAI_API_KEY를 설정하세요. Node.js를 사용하는 경우 파일 이름을 server.mjs로 지정하세요. openai와 express를 설치하려면 npm install openai express를 실행하세요. Python을 사용하는 경우 openai를 설치하세요. 이 예제 서버는 127.0.0.1에 바인딩되어 http://localhost:3000에서 보낸 세션 요청을 수락하고, 실행 디렉터리에 있는 index.html을 제공합니다.
Go를 사용하는 경우 예제를 main.go로 저장하고 go mod init live-demo로 모듈을 초기화한 다음 github.com/openai/openai-go/v3@v3.85.1을 go get으로 설치하세요. Go 1.26 이상에서 go run .을 실행하세요. Java를 사용하는 경우 Java 17 이상에서 Maven 종속성 com.openai:openai-java:4.95.0을 추가하고 LiveConnectionWebrtcExample 클래스를 실행하세요. Ruby 3.3 이상에서 openai 0.110.0과 webrick을 설치하려면 gem install openai -v 0.110.0과 gem install webrick을 실행하세요. 예제를 server.rb로 저장하고 ruby server.rb를 실행하세요.
아래에서 서버 언어를 선택하세요. 어느 베리언트를 선택해도 포트 3000에서 index.html을 제공하며, 세션 엔드포인트는 /api/session으로 동일합니다. Live를 지원하는 SDK 버전을 사용하고 한 번에 하나의 베리언트만 실행하세요.
import express from "express";
import OpenAI from "openai";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
const app = express();
const client = new OpenAI({ maxRetries: 0 });
const port = 3000;
const origin = `http://localhost:${port}`;
const indexPath = resolve("index.html");
app.use(express.json({ limit: "64kb" }));
app.get("/", async (_request, response) => {
response.type("html").send(await readFile(indexPath, "utf8"));
});
// Local-only demo. Add your application's authentication and authorization
// before exposing session creation to other users.
app.post("/api/session", async (request, response) => {
if (request.headers.origin !== origin) {
response.status(403).json({ error: "Unexpected request origin" });
return;
}
if (typeof request.body?.sdp !== "string" || !request.body.sdp.trim()) {
response.status(400).json({ error: "An SDP offer is required" });
return;
}
if (!process.env.OPENAI_API_KEY) {
response.status(503).json({ error: "Set OPENAI_API_KEY on the server" });
return;
}
try {
const result = await client.live.create({
session: {
model: "gpt-live-1",
instructions:
"Be concise. Delegate requests needing current information to the backend, which can search the web.",
delegation: {
type: "responses",
responses: {
model: "gpt-5.6-terra",
instructions:
"Use web search when current facts are needed. Return concise, grounded results for a spoken conversation.",
tools: [{ type: "web_search" }],
tool_choice: "auto",
},
},
},
transport: {
type: "webrtc",
sdp: request.body.sdp,
},
});
// Preserve the SDK's typed session ID and SDP answer.
response.status(201).json(result);
} catch (error) {
if (!(error instanceof OpenAI.APIError)) throw error;
console.error("Live session creation failed", error.status);
response
.status(error.status ?? 502)
.json({ error: "Live session creation failed" });
}
});
app.listen(port, "127.0.0.1", () => console.log(`Open ${origin}`));다른 사용자가 서버에 접근할 수 있도록 공개하기 전에 /api/session에 애플리케이션의 인증, 인가, 요청 제한, HTTPS를 적용하세요. 이 로컬 예제의 출처 검사는 사용자를 인증하지 않습니다.
브라우저 클라이언트 만들기
서버 실행 디렉터리에 index.html을 만드세요.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>GPT-Live connection</title>
</head>
<body>
<script type="module">
// Paste the browser code below here.
</script>
</body>
</html>다음 코드를 모듈 스크립트 안에 붙여 넣으세요. 이 코드는 대화를 시작하고 종료하는 버튼을 추가하고, 마이크와 오디오 출력을 연결하며, 세션 이벤트를 처리합니다. /api/session은 애플리케이션 서버의 라우트입니다.
const start = document.createElement("button");
start.textContent = "Start conversation";
const stop = document.createElement("button");
stop.textContent = "End conversation";
stop.disabled = true;
const status = document.createElement("p");
const audio = new Audio();
audio.autoplay = true;
audio.controls = true;
document.body.append(start, stop, status, audio);
let peer;
let events;
let microphone;
let closeTimeout;
let ready = false;
let finalized = false;
function cleanup() {
clearTimeout(closeTimeout);
microphone?.getTracks().forEach((track) => track.stop());
const closingEvents = events;
events = undefined;
closingEvents?.close();
peer?.close();
audio.srcObject = null;
ready = false;
start.disabled = false;
stop.disabled = true;
}
start.addEventListener("click", async () => {
start.disabled = true;
finalized = false;
status.textContent = "Connecting…";
try {
const connection = new RTCPeerConnection();
peer = connection;
connection.addEventListener("track", (event) => {
audio.srcObject = new MediaStream([event.track]);
audio.play().catch(() => {
status.textContent =
"Select play on the audio controls to hear the assistant.";
});
});
microphone = await navigator.mediaDevices.getUserMedia({ audio: true });
for (const track of microphone.getAudioTracks()) {
connection.addTrack(track, microphone);
}
// Create the event channel before creating the SDP offer.
events = connection.createDataChannel("oai-events");
events.addEventListener("message", ({ data }) => {
const event = JSON.parse(data);
if (event.type === "session.started") {
ready = true;
stop.disabled = false;
status.textContent = "Connected: " + event.session.id;
} else if (event.type === "session.closed") {
finalized = true;
console.log("Final session usage", event.usage);
status.textContent = "Conversation ended.";
cleanup();
} else {
// Save transcript and nested Responses events as needed by your app.
console.log(event);
}
});
events.addEventListener("close", (event) => {
if (event.target !== events) return;
if (!finalized) {
status.textContent = "Disconnected without final session usage.";
cleanup();
}
});
const offer = await connection.createOffer();
await connection.setLocalDescription(offer);
if (connection.iceGatheringState !== "complete") {
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
connection.removeEventListener("icegatheringstatechange", onState);
reject(new Error("Timed out while gathering ICE candidates"));
}, 10_000);
function onState() {
if (connection.iceGatheringState !== "complete") return;
clearTimeout(timeout);
connection.removeEventListener("icegatheringstatechange", onState);
resolve(undefined);
}
connection.addEventListener("icegatheringstatechange", onState);
onState();
});
}
const sdp = connection.localDescription?.sdp;
if (!sdp) throw new Error("Missing local SDP offer");
const response = await fetch("/api/session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sdp }),
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log("Created session", result.session.id);
await connection.setRemoteDescription({
type: "answer",
sdp: result.transport.sdp,
});
// The HTTP request started this session. Do not send session.start here.
} catch (error) {
status.textContent =
error instanceof Error ? error.message : String(error);
cleanup();
}
});
stop.addEventListener("click", () => {
if (!ready || !events || events.readyState !== "open") return;
stop.disabled = true;
status.textContent = "Finishing the conversation…";
// The session.closed handler is already registered. Keep media and events
// alive while pending work drains; only clean up after the final event.
events.send(JSON.stringify({ type: "session.close" }));
closeTimeout = setTimeout(() => {
status.textContent = "Incomplete finalization: no session.closed event.";
cleanup();
}, 15_000);
});선택한 서버를 실행하고(node server.mjs 또는 python server.py) http://localhost:3000을 연 다음 Start conversation을 선택하세요. 상태가 Connected로 바뀌면 최신 정보가 필요한 질문을 해서 호스팅형 웹 검색을 테스트하세요. 브라우저가 자동 재생을 차단하면 오디오 재생 컨트롤을 사용하세요.
세션 응답 읽기
요청이 성공하면 HTTP 201과 함께 세션 ID와 SDP 응답이 담긴 JSON이 반환됩니다.
{
"session": { "id": "live_123" },
"transport": { "type": "webrtc", "sdp": "<SDP answer>" }
}result.session.id를 읽고 result.transport.sdp를 setRemoteDescription에 전달하세요. 세션 ID의 내부 구조를 해석하지 말고 접두사를 포함한 전체 값을 그대로 유지하세요.
연결을 시작할 때 지연 시간을 줄이는 방법은 WARP를 사용한 WebRTC 연결을 참고하세요.
미디어와 이벤트 처리하기
미디어 트랙으로 마이크 오디오를 보내고 모델이 생성한 음성을 받으세요. WebRTC는 SDP를 통해 오디오 형식을 협상하므로 세션 설정에서 audio.format을 생략하세요. 데이터 채널로 session.input_audio.append를 보내지 마세요. session.output_audio.delta도 이 채널로 수신되지 않습니다.
전사 내용의 증분 업데이트, 세션 명령어, 중첩된 response.event 메시지는 데이터 채널로 주고받으세요. 전사 처리와 수명 주기 이벤트는 세션 관리를 참고하세요. 서버에 별도의 이벤트 연결이 필요하면 서버 측 제어를 참고하세요.
대화를 종료하려면 session.close를 보내고 이벤트를 계속 수신하세요. session.closed를 받은 후 피어 연결을 닫고 마이크 트랙을 종료하세요. 예제에서는 명령어를 보내기 전에 마지막 이벤트를 수신할 리스너를 등록합니다. 마지막 이벤트를 받기 전에 연결이 끊기거나 시간 초과가 발생하면 최종 사용량을 확인할 수 없습니다. 최종 사용량 처리 방법은 사용량 확인 및 정상 종료를 참고하세요.
WebRTC는 실시간 애플리케이션을 구축하는 데 쓰이는 강력한 표준 인터페이스 모음입니다. OpenAI Realtime API는 WebRTC 피어 연결을 통해 실시간 모델에 연결하는 기능을 지원합니다.
브라우저 기반 음성-음성 변환 애플리케이션을 개발한다면 음성 에이전트부터 살펴보세요. 이 가이드에서는 Realtime 세션을 관리하기 위해 Agents SDK가 제공하는 상위 수준의 헬퍼와 API를 설명합니다. WebRTC 인터페이스는 강력하고 유연하지만 Agents SDK보다 낮은 수준에서 동작합니다.
웹 브라우저나 모바일 기기 같은 클라이언트에서 Realtime 모델에 연결할 때는 더 일관된 성능을 위해 WebSockets보다 WebRTC를 권장합니다.
WebRTC 기반 사용자 인터페이스를 구축하는 방법은 MDN 문서를 참고하세요.
개요
Realtime API는 브라우저에서 연결하는 두 가지 방식을 지원합니다. 통합 인터페이스를 사용하거나 임시 API 키(OpenAI REST API로 생성)를 사용할 수 있습니다. 설정을 간소화하고 연결 시간을 줄이려면 통합 인터페이스를 사용하세요. 이 방식에서는 세션을 초기화할 때 애플리케이션 서버를 반드시 거칩니다.
통합 인터페이스로 연결하기
웹 브라우저 클라이언트에서 통합 인터페이스로 WebRTC 연결을 초기화하는 절차는 다음과 같습니다.
- 브라우저가 WebRTC 피어 연결에서 가져온 SDP 데이터를 개발자가 관리하는 서버로 보냅니다.
- 서버는 SDP와 세션 설정을 하나의 멀티파트 폼에 담아 OpenAI Realtime API로 보내고 서버의 표준 API 키로 요청을 인증합니다.
통합 인터페이스로 세션 만들기
통합 인터페이스로 Realtime API 세션을 만들려면 /v1/realtime/calls에 요청을 보내는 작은 서버 측 애플리케이션을 만들거나 기존 애플리케이션에 이 기능을 추가하세요. 백엔드 서버에서 표준 API 키로 요청을 인증하세요.
애플리케이션 서버에서 다음과 같이 Realtime API 세션을 만드세요.
import express from "express";
const app = express();
// Parse raw SDP payloads posted from the browser
app.use(express.text({ type: ["application/sdp", "text/plain"] }));
const sessionConfig = JSON.stringify({
type: "realtime",
model: "gpt-realtime-2.1",
audio: { output: { voice: "marin" } },
});
// An endpoint which creates a Realtime API session.
app.post("/session", async (req, res) => {
const fd = new FormData();
fd.set("sdp", req.body);
fd.set("session", sessionConfig);
try {
const r = await fetch("https://api.openai.com/v1/realtime/calls", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
"OpenAI-Safety-Identifier": "hashed-user-id",
},
body: fd,
});
// Send back the SDP we received from the OpenAI REST API
const sdp = await r.text();
res.set(
"Content-Type",
r.ok ? "application/sdp" : (r.headers.get("Content-Type") ?? "text/plain")
);
const location = r.headers.get("Location");
if (location) res.set("Location", location);
res.status(r.status).send(sdp);
} catch (error) {
console.error("Session creation error:", error);
res.status(500).json({ error: "Failed to create session" });
}
});
app.listen(3000);애플리케이션에서 최종 사용자마다 안전 식별자를 할당한다면 이 서버 측 요청의 OpenAI-Safety-Identifier 헤더에 해당 식별자를 넣으세요. 내부 사용자 ID의 해시값처럼 동일한 사용자에 대해 일정하게 유지되면서 개인정보를 보호하는 값을 사용하세요. 헤더는 신뢰할 수 있는 백엔드에서만 설정하세요.
서버에 연결하기
브라우저에서는 표준 WebRTC API를 사용해 애플리케이션 서버를 거쳐 Realtime API에 연결할 수 있습니다. 클라이언트는 POST 요청으로 SDP 데이터를 서버에 보냅니다.
// Create a peer connection
const pc = new RTCPeerConnection();
// Set up to play remote audio from the model
audioElement.current = document.createElement("audio");
audioElement.current.autoplay = true;
pc.ontrack = (e) => (audioElement.current.srcObject = e.streams[0]);
// Add local audio track for microphone input in the browser
const ms = await navigator.mediaDevices.getUserMedia({
audio: true,
});
pc.addTrack(ms.getTracks()[0]);
// Set up data channel for sending and receiving events
const dc = pc.createDataChannel("oai-events");
// Start the session using the Session Description Protocol (SDP)
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const sdpResponse = await fetch("/session", {
method: "POST",
body: offer.sdp,
headers: {
"Content-Type": "application/sdp",
},
});
const answer = {
type: "answer",
sdp: await sdpResponse.text(),
};
await pc.setRemoteDescription(answer);임시 토큰으로 연결하기
웹 브라우저 클라이언트에서 임시 API 키로 WebRTC 연결을 초기화하는 절차는 다음과 같습니다.
- 브라우저가 개발자가 관리하는 서버에 임시 API 키 발급을 요청합니다.
- 개발자의 서버는 표준 API 키를 사용해 OpenAI REST API에 임시 키를 요청하고 새로 발급된 키를 브라우저에 반환합니다.
- 브라우저는 임시 키로 OpenAI Realtime API에서 직접 세션 인증을 받고 WebRTC 피어 연결을 설정합니다.
임시 토큰 만들기
클라이언트에서 사용할 임시 토큰을 만들려면 작은 서버 측 애플리케이션을 만들거나 기존 애플리케이션에 기능을 추가해 OpenAI REST API에 임시 키를 요청하세요. 백엔드 서버에서 표준 API 키로 요청을 인증하세요.
애플리케이션 서버에서 REST API를 사용해 다음과 같이 임시 API 키를 만드세요.
import express from "express";
const app = express();
const sessionConfig = JSON.stringify({
session: {
type: "realtime",
model: "gpt-realtime-2.1",
audio: {
output: {
voice: "marin",
},
},
},
});
// An endpoint which would work with the client code above - it returns
// the contents of a REST API request to this protected endpoint
app.get("/token", async (req, res) => {
res.set("Cache-Control", "no-store");
try {
const response = await fetch(
"https://api.openai.com/v1/realtime/client_secrets",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json",
"OpenAI-Safety-Identifier": "hashed-user-id",
},
body: sessionConfig,
}
);
const data = await response.json();
res.status(response.status).json(data);
} catch (error) {
console.error("Token generation error:", error);
res.status(500).json({ error: "Failed to generate token" });
}
});
app.listen(3000);HTTP 요청을 주고받을 수 있는 플랫폼이라면 어디서든 이런 서버 엔드포인트를 만들 수 있습니다. 표준 OpenAI API 키는 서버에서만 사용하세요. 브라우저에서는 사용하지 마세요.
임시 토큰을 사용할 때는 클라이언트 시크릿을 생성하는 서버 측 요청에 OpenAI-Safety-Identifier를 설정하세요. Realtime API가 식별자를 발급된 임시 토큰에 연결하므로 나중에 브라우저가 해당 토큰으로 연결할 때 안전 식별자를 따로 보낼 필요가 없습니다.
서버에 연결하기
브라우저에서는 표준 WebRTC API를 사용해 임시 토큰으로 Realtime API에 연결할 수 있습니다. 클라이언트는 먼저 서버 엔드포인트에서 토큰을 가져온 다음 SDP 데이터와 임시 토큰을 POST 요청으로 Realtime API에 보냅니다.
// Get a session token for OpenAI Realtime API
const tokenResponse = await fetch("/token");
const data = await tokenResponse.json();
const EPHEMERAL_KEY = data.value;
// Create a peer connection
const pc = new RTCPeerConnection();
// Set up to play remote audio from the model
audioElement.current = document.createElement("audio");
audioElement.current.autoplay = true;
pc.ontrack = (e) => (audioElement.current.srcObject = e.streams[0]);
// Add local audio track for microphone input in the browser
const ms = await navigator.mediaDevices.getUserMedia({
audio: true,
});
pc.addTrack(ms.getTracks()[0]);
// Set up data channel for sending and receiving events
const dc = pc.createDataChannel("oai-events");
// Start the session using the Session Description Protocol (SDP)
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const sdpResponse = await fetch("https://api.openai.com/v1/realtime/calls", {
method: "POST",
body: offer.sdp,
headers: {
Authorization: `Bearer ${EPHEMERAL_KEY}`,
"Content-Type": "application/sdp",
},
});
const answer = {
type: "answer",
sdp: await sdpResponse.text(),
};
await pc.setRemoteDescription(answer);WARP로 연결 지연 시간 줄이기
WebRTC Abridged Roundtrip Protocol(WARP)은 Realtime API 음성 세션을 시작하는 데 걸리는 시간을 줄여 줍니다. 최적화 기능을 개별적으로 활성화하거나 함께 사용해 전체 WARP 핸드셰이크를 수행할 수 있습니다.
네이티브 클라이언트 설정, 브라우저 지원, 오리진 트라이얼 참여 방법, 통합 연결 플로우는 WARP를 사용한 WebRTC 연결을 참고하세요.
이벤트 주고받기
Realtime API 세션은 개발자가 보내는 클라이언트 이벤트와 Realtime API가 세션 수명 주기의 변화를 알리기 위해 보내는 서버 이벤트를 함께 사용해 관리합니다.
WebRTC로 Realtime 모델에 연결하면 WebSockets를 사용할 때처럼 모델의 오디오 이벤트를 하나씩 처리할 필요가 없습니다. 위와 같이 구성하면 WebRTC 피어 연결 객체가 이 작업을 모두 처리합니다.
그 밖의 클라이언트 및 서버 이벤트는 WebRTC 피어 연결의 데이터 채널로 주고받을 수 있습니다.
// Reuse dc from the preceding browser connection example.
// Listen for server events
dc.addEventListener("message", (e) => {
const event = JSON.parse(e.data);
console.log(event);
});
// Send client events
const event = {
type: "conversation.item.create",
item: {
type: "message",
role: "user",
content: [
{
type: "input_text",
text: "hello there!",
},
],
},
};
const sendGreeting = () => dc.send(JSON.stringify(event));
if (dc.readyState === "open") {
sendGreeting();
} else if (dc.readyState === "connecting") {
dc.addEventListener("open", sendGreeting, { once: true });
} else {
throw new Error("The data channel has closed before greeting was sent.");
}Realtime 대화를 관리하는 방법은 Realtime 대화 가이드를 참고하세요.
간단한 예제 앱에서 WebRTC로 Realtime API를 사용해 보세요.