使用する API を選ぶと、接続手順とセッションイベントを確認できます。
ブラウザから GPT-Live への接続
ブラウザ、モバイル、サーバーの各アプリケーションで GPT-Live と音声をやり取りするには、WebRTC を推奨します。このガイドでは、ブラウザを例に接続手順を説明します。マイク入力と生成された音声は、接続時にネゴシエーションしたメディアトラックで送受信します。文字起こし、セッションの更新、タスクの委任に関する JSON イベントはデータチャネルで送受信します。
ブラウザは Session Description Protocol(SDP)のオファーを作成します。アプリケーションサーバーはプロジェクトの API キーを使って POST /v1/live/sessions にオファーを送信し、アンサーを受け取ります。キーとセッション設定は信頼できるサーバーで管理してください。
前提条件
以下を用意してください。
- GPT-Live にアクセスできるプロジェクトの API キー
- 使用する SDK のサンプルを実行できるサーバー環境(Node.js のサンプルには Node.js 22.6 以降が必要)
- HTTPS または localhost でページを開き、マイクの使用を許可したブラウザ
このサンプルでは、Responses へのタスク委任に gpt-5.6-terra とホスト型ウェブ検索を使用します。バックエンドへの指示とアプリケーションのツールについては、タスクの委任とツールを参照してください。音声とバックエンドの使用量については、コストの最適化を参照してください。
接続の流れ
- ユーザーの操作をきっかけにマイクへのアクセス許可を求め、マイクのトラックをピア接続に追加します。
- SDP オファーを作成する前に、データチャネルを作成し、イベントリスナーを登録します。
- ローカル記述を設定し、ICE 候補の収集が完了するのを待ってから、オファーをサーバーに送信します。
- サーバーから、
sessionとtransport: { type: "webrtc", sdp: ... }を含む JSON を 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 を使用し、一度に実行するサンプルは 1 つだけにしてください。
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}`));サーバーを他のユーザーに公開する前に、アプリケーションの認証、認可、リクエスト制限、HTTPS を使って /api/session を保護してください。このローカル用サンプルのオリジンチェックでは、ユーザーは認証されません。
ブラウザクライアントの作成
サーバーを実行するディレクトリに 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 で生成)の 2 つの方式を利用できます。セットアップを簡単にし、接続を速くするには、統合インターフェースを使用してください。この方式では、セッションの初期化にアプリケーションサーバーを経由する必要があります。
統合インターフェースを使った接続
ウェブブラウザをクライアントとして、統合インターフェースで 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 接続を試せます。