OpenAI 电话与 SIP
选择所用API,查看对应的连接步骤与事件。
通过SIP中继把呼入和呼出电话连接到GPT-Live。提供商与OpenAI交换音频,后端处理会话配置、通话控制和业务逻辑。SIP信令使用TLS,GPT-Live要求通话音频使用SRTP。
- 呼入:接收来电webhook,再接受或拒绝。
- 呼出:创建会话,让提供商拨打电话号码,不需要来电webhook或accept请求。
后端需要接收会话事件或发送命令时,使用旁路连接附加到既有对话,音频仍由SIP承载。每项动作只分配一个处理器,避免重复webhook或多连接观察到同一事件时执行两次工具。
处理呼入
把SIP提供商的来电路由至OpenAI项目,后端接收webhook并决定是否接受。
配置电话号码与中继
使用Twilio等SIP中继提供商。向提供商购买号码后:
- 打开项目设置,在Webhooks下创建订阅live.transport.incoming的webhook端点。
- 在General中取得以proj_开头的项目ID,必须与webhook所属项目一致。
- 将提供商中继指向
sip:$PROJECT_ID@sip.api.openai.com;transport=tls。欧洲数据驻留改用sip:$PROJECT_ID@sip-eu.api.openai.com;transport=tls。
使用前确认项目已启用GPT-Live SIP支持,且中继确实路由至该项目。
接收来电
OpenAI收到项目来电后,向webhook发送live.transport.incoming。先验证签名并对投递去重,再接受或拒绝。
data.type为sip时表示SIP通话,data.session_id是后续所有Live通话操作应原样使用的会话ID。data.sip_headers只是未受信任的来电者元信息,不作为授权依据。
webhook请求体示例:
{
"object": "event",
"id": "evt_abc123",
"type": "live.transport.incoming",
"created_at": 1750287018,
"data": {
"type": "sip",
"session_id": "live_123",
"sip_headers": [
{ "name": "From", "value": "sip:+14155550123@sip.example.com" },
{ "name": "To", "value": "sip:+18005550100@sip.example.com" },
{ "name": "Call-ID", "value": "call-123@example.com" }
]
}
}
created_at为以秒计的Unix时间戳。投递头webhook-id用于去重,webhook-timestamp表示该次投递尝试时间,webhook-signature用于签名验证。服务端处理见webhook签名验证。
既有集成可能仍收到没有data.type的旧live.call.incoming。迁移时兼容两种名称,并保留旧订阅直到旧投递和重试处理完。同一待接来电也可能产生Realtime webhook;接受/拒绝决定只指定一个处理器,不通过两套API同时接受。
接受或拒绝
应用自己的授权和路由规则仍须执行。接受来电时,认证后发送POST /v1/live/sessions/{session_id}/accept,请求体包含顶层session对象:
{
"session": {
"type": "live",
"model": "gpt-live-1",
"instructions": "You are answering an inbound support call.",
"audio": { "output": { "voice": "marin" } },
"delegation": { "type": "client" }
}
}
通话控制由可信后端使用Authorization: Bearer $OPENAI_API_KEY。接受时选择声音和委派模式。SIP协商音频格式,因此省略audio.format。本例选择client委派,后端需要实际处理委派;client和Responses配置见委派与工具。
初始化成功后,accept返回200 OK且正文为空。确认接受之前先处理HTTP错误。
拒绝来电时发送POST /v1/live/sessions/{session_id}/reject,提供SIP状态,例如忙线使用{"status_code":486}。状态必须为300至699的整数。第一次接受或拒绝决定生效,后来的竞争决定返回decision_already_made。
附加后端
接受后,以已接受的会话ID、同一项目认证和连接头,连接wss://api.openai.com/v1/live/sessions/{session_id}/attach。不要再发送session.start。
SIP承载音频,旁路用于转录、委派、工具、命令和音频映射。即使多个连接看到同一事件,每种副作用也只设一个执行者。连接后可使用下方活动通话控制发送按键音、转接或挂断。
发起呼出
通过创建会话让提供商拨号,电话网络连接由提供商处理,对话由GPT-Live承载。呼出SIP现通过Live API向所有组织提供,不使用Realtime的通话创建端点。
配置中继
中继需支持TLS信令和SDES-SRTP媒体;拨号前在提供商设置启用SRTP。每次请求提供以下配置:
| 字段 | 含义 |
|---|---|
| transport.destination | E.164格式被叫号码,如+14155550123;不支持SIP URI目标 |
| transport.trunk.provider_url | 如sips:sip.example.com:5061,默认端口5061,可选;transport=tcp |
| transport.trunk.auth.type | SIP Digest认证使用digest |
| transport.trunk.auth.username | 提供商SIP用户名 |
| transport.trunk.auth.password | 提供商SIP密码 |
| transport.trunk.caller_number | 发送给提供商的E.164格式主叫号码 |
提供商地址必须使用sips:以启用TLS,不包含凭证、路径、URI头或其他URI参数。本地主机名及私有/本地IP字面值会被拒绝。OpenAI密钥和SIP凭证都保留在服务器。
创建会话
向POST /v1/live/sessions发送会话配置和transport.type: sip,创建时选择声音与委派模式,省略由SIP协商的audio.format。
下例依赖curl和jq。设置服务端环境变量OPENAI_API_KEY、SIP_USERNAME和SIP_PASSWORD,把示例提供商和号码换为自己的值;client委派仍需后端实现。
## [中文] 从服务端环境读取凭证,用jq构造SIP会话请求,再向Live创建会话;200响应不代表被叫已接听。
jq -n \
--arg username "$SIP_USERNAME" \
--arg password "$SIP_PASSWORD" \
'{
"session": {
"model": "gpt-live-1",
"instructions": "Help the user schedule an appointment.",
"audio": { "output": { "voice": "marin" } },
"delegation": { "type": "client" }
},
"transport": {
"type": "sip",
"destination": "+14155550123",
"trunk": {
"provider_url": "sips:sip.example.com:5061",
"auth": {
"type": "digest",
"username": $username,
"password": $password
},
"caller_number": "+14155550100"
}
}
}' | curl https://api.openai.com/v1/live/sessions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @-
会话初始化后返回200 OK:
{
"session": { "id": "live_123" },
"transport": { "type": "sip" }
}
响应不表示被叫已接听,也不包含SDP或中继凭证。后续旁路和控制操作原样保留session.id。呼出无需来电webhook或accept。
监控与结束通话
使用OpenAI密钥附加到上述attach端点,不再发送session.start。SIP承载音频,旁路提供对话事件、委派及呼叫进度。
| 事件 | 含义 |
|---|---|
| transport.ringing | 提供商报告振铃或早期媒体 |
| transport.answered | 已接听并建立媒体 |
| transport.failed | 初始化后建连失败,检查error.code和error.message |
进度事件均含event_id和session_id。创建后立即附加:旁路只重放此前3秒事件,迟接可能错过进度;重放保持原event_id,按该ID去重。
连接后使用活动通话控制,保持旁路直到session.closed和最终用量。见用量与正常关闭。
限制与错误
振铃最多3分钟,接通后最多2小时,不能在创建请求中调整。无效会话配置直接由创建请求返回;传输建立失败可能为502,初始化超时可能为504。创建成功后继续监听transport.failed以发现异步失败。
管理活动通话
以下操作同时适用于呼入与呼出。
发送与接收按键
来电者按键时,旁路收到transport.dtmf.received。通过旁路WebSocket发送transport.dtmf.send客户端事件可向中继发DTMF音;成功后收到transport.dtmf.sent。
提供event_id时,通知用client_event_id对应;失败时error事件包含error.client_event_id。
转接或结束
转接使用POST /v1/live/sessions/{session_id}/refer,正文如{"target_uri":"sip:agent@example.com"}。挂断使用POST /v1/live/sessions/{session_id}/hangup,不发请求体。成功都返回200 OK、空正文。
转接目标写入SIP Refer-To头,可为tel:+14155550123等电话URI或sip:agent@example.com等SIP URI,后续转接流程归提供商。保持旁路直到session.closed携带最终用量,再按正常关闭说明处理资源。
SIP网络要求
信令和媒体使用分离网络路径,均需放行:
- SIP信令:sip.api.openai.com和sip-eu.api.openai.com为GeoIP路由入口,允许向DNS返回地址的5061端口发出TCP/TLS流量。
- SRTP媒体:协商SDP指定独立媒体IP和UDP端口,允许以下CIDR的双向UDP SRTP:13.79.45.80/28、23.98.140.64/28、40.67.149.176/28、40.83.204.240/28。
GPT-Live后续步骤
- 提示词:编写语音智能体指令。
- Webhook与服务端控制:从后端管理会话。
- 委派与工具:连接推理与工具后端。
- 会话管理:处理转录、状态和关闭。
- 延迟与成本:理解用量与延迟。
- 合作伙伴集成:查找提供商专用配置和示例。
Realtime API:SIP 是通过互联网拨打电话的协议。结合 Realtime API,可将呼入电话直接路由至 API。
概览
把电话号码接入 Realtime 时,使用 Twilio 等 SIP 中继服务商,将电话转为 IP 流量。购买号码后执行以下步骤。
在 platform.openai.com 的设置 > Project > Webhooks 创建来电 webhook。再把 SIP 中继指向 sip:$PROJECT_ID@sip.api.openai.com;transport=tls,项目 ID 使用配置 webhook 的项目。欧洲数据驻留使用 sip:$PROJECT_ID@sip-eu.api.openai.com;transport=tls。在设置 > Project > General 查看以 proj_ 开头的项目 ID。
OpenAI 收到该项目的 SIP 流量时,发出 realtime.call.incoming:
来源示例 4(text):字段名、事件类型和示意值保留原样,含义见相邻正文;片段中的省略号不表示完整可运行程序。
POST https://my_website.com/webhook_endpoint
user-agent: OpenAI/1.0 (+https://platform.openai.com/docs/webhooks)
content-type: application/json
webhook-id: wh_685342e6c53c8190a1be43f081506c52 # unique id for idempotency
webhook-timestamp: 1750287078 # timestamp of delivery attempt
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4= # signature to verify authenticity from OpenAI
{
"object": "event",
"id": "evt_685343a1381c819085d44c354e1b330e",
"type": "realtime.call.incoming",
"created_at": 1750287018, // Unix timestamp
"data": {
"call_id": "some_unique_id",
"sip_headers": [
{ "name": "From", "value": "sip:+142555512112@sip.example.com" },
{ "name": "To", "value": "sip:+18005551212@sip.example.com" },
{ "name": "Call-ID", "value": "03782086-4ce9-44bf-8b0d-4e303d2cc590"}
]
}
}
用 webhook 的 call_id 接受或拒绝。接受时提供指令、声音等 Realtime 会话配置。建立后可用 WebSocket 监控。以下介绍接受、拒绝、监控、转接和挂断 API。
接受来电
用接受端点批准呼入,并配置应答会话。参数与创建客户端密钥相同,应在桥接到模型前配置模型、声音、工具或指令。
来源示例 5(bash):
程序文字译释228:你是Alex,Example Corp的友好礼宾助手。
## [中文] 示例 5:接受来电;以下保留本语言原始示例。
## [中文] 本段涉及的 HTTP 端点:/v1/realtime/calls/$CALL_ID/accept;认证和请求体保持原示例形式。
curl -X POST "https://api.openai.com/v1/realtime/calls/$CALL_ID/accept" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "realtime",
"model": "gpt-realtime-2.1",
"instructions": "You are Alex, a friendly concierge for Example Corp."
}'
路径必须包含 realtime.call.incoming 的 call_id,每次请求都包含示例中的 Authorization 头。SIP 分支开始振铃且 Realtime 会话正在建立时返回 200 OK。
拒绝来电
不处理来电,例如国家代码不受支持时,用拒绝端点。提供路径 call_id,可在 JSON 中加 SIP status_code,如 486 表示忙,控制向运营商的回复。
来源示例 6(bash):
## [中文] 示例 6:拒绝来电;以下保留本语言原始示例。
## [中文] 本段涉及的 HTTP 端点:/v1/realtime/calls/$CALL_ID/reject;认证和请求体保持原示例形式。
curl -X POST "https://api.openai.com/v1/realtime/calls/$CALL_ID/reject" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status_code": 486}'
省略状态时默认 603 Decline。OpenAI 发送 SIP 回复后,成功请求返回 200 OK。
监控通话事件
接受后,为同一会话打开 WebSocket 收事件、发命令。通过 call_id 附加既有通话时,不使用 model 参数,因为已在接受端点配置。
WebSocket 请求
GET wss://api.openai.com/v1/realtime?call_id={call_id}
查询参数
| 参数 | 类型 | 说明 |
|---|---|---|
call_id |
string | realtime.call.incoming webhook 的标识。 |
请求头
Authorization: Bearer YOUR_API_KEY
WebSocket 与其他 Realtime 连接行为相同,可发 response.create 等客户端事件控制通话,监听服务端事件追踪进度。见 Webhook 与服务端控制。
来源示例 7(javascript):
// [中文] 显式请求创建或继续响应,使用本段实际设置的模态、上下文与工具字段;发送请求不等同于收到完成事件。
import WebSocket from "ws";
const callId = "rtc_u1_9c6574da8b8a41a18da9308f4ad974ce";
const ws = new WebSocket(`wss://api.openai.com/v1/realtime?call_id=${callId}`, {
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
},
});
ws.on("open", () => {
ws.send(
JSON.stringify({
type: "response.create",
})
);
});
转接通话
使用转接端点,提供 call_id 与放入 SIP Refer-To 的 target_uri,例如 tel:+14155550123 或 sip:agent@example.com。
来源示例 8(bash):
## [中文] 示例 8:转接通话;以下保留本语言原始示例。
## [中文] 本段涉及的 HTTP 端点:/v1/realtime/calls/$CALL_ID/refer;认证和请求体保持原示例形式。
curl -X POST "https://api.openai.com/v1/realtime/calls/$CALL_ID/refer" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"target_uri": "tel:+14155550123"}'
REFER 转发到提供商后返回 200 OK,其余通话流程由下游系统处理。
挂断通话
应用需要断开来电者时,使用挂断端点,可终止 SIP 和 WebRTC Realtime 会话。
来源示例 9(bash):
## [中文] 示例 9:挂断通话;以下保留本语言原始示例。
## [中文] 本段涉及的 HTTP 端点:/v1/realtime/calls/$CALL_ID/hangup;认证和请求体保持原示例形式。
curl -X POST "https://api.openai.com/v1/realtime/calls/$CALL_ID/hangup" \
-H "Authorization: Bearer $OPENAI_API_KEY"
API 开始拆除通话时返回 200 OK。
SIP 信令与媒体 IP 范围
Realtime SIP 的信令与媒体使用不同网络路径,应分别放行。
SIP 信令
sip.api.openai.com、sip-eu.api.openai.com 按 GeoIP 路由。网络须允许向 DNS 返回地址的 5061 端口发起 TCP/TLS 出站流量。
SRTP 媒体
API 在协商 SDP 中指定独立媒体 IP 和 UDP 端口。网络须允许下列 CIDR 的双向 UDP SRTP:
13.79.45.80/2823.98.140.64/2840.67.149.176/2840.83.204.240/28
服务端示例
以下 realtime.call.incoming 处理器接受来电后,记录 Realtime API 的全部事件。
Ruby 示例需设置 OPENAI_API_KEY、OPENAI_WEBHOOK_SECRET,并执行 gem install openai webrick async-websocket 安装依赖。
来源示例 10(python):
程序文字译释229:你是客服人员。
程序文字译释230:对用户说:“感谢来电,有什么可以帮助你?”
程序文字译释231:显示从WebSocket收到的响应。
程序文字译释232:显示WebSocket错误。
程序文字译释233:签名无效。
## [中文] 显式请求创建或继续响应,使用本段实际设置的模态、上下文与工具字段;发送请求不等同于收到完成事件。
from flask import Flask, request, Response, jsonify, make_response
from openai import OpenAI, InvalidWebhookSignatureError
import asyncio
import json
import os
import requests
import time
import threading
import websockets
app = Flask(__name__)
client = OpenAI(webhook_secret=os.environ["OPENAI_WEBHOOK_SECRET"])
AUTH_HEADER = {"Authorization": "Bearer " + os.environ["OPENAI_API_KEY"]}
call_accept = {
"type": "realtime",
"instructions": "You are a support agent.",
"model": "gpt-realtime-2.1",
}
response_create = {
"type": "response.create",
"response": {
"instructions": ("Say to the user 'Thank you for calling, how can I help you'")
},
}
async def websocket_task(call_id):
try:
async with websockets.connect(
"wss://api.openai.com/v1/realtime?call_id=" + call_id,
additional_headers=AUTH_HEADER,
) as websocket:
await websocket.send(json.dumps(response_create))
while True:
response = await websocket.recv()
print(f"Received from WebSocket: {response}")
except Exception as e:
print(f"WebSocket error: {e}")
@app.route("/", methods=["POST"])
def webhook():
try:
event = client.webhooks.unwrap(request.data, request.headers)
except (InvalidWebhookSignatureError, ValueError) as e:
print("Invalid signature", e)
return Response("Invalid signature", status=400)
if event.type == "realtime.call.incoming":
requests.post(
"https://api.openai.com/v1/realtime/calls/"
+ event.data.call_id
+ "/accept",
headers={**AUTH_HEADER, "Content-Type": "application/json"},
json=call_accept,
).raise_for_status()
threading.Thread(
target=lambda: asyncio.run(websocket_task(event.data.call_id)),
daemon=True,
).start()
return Response(status=200)
if __name__ == "__main__":
app.run(port=8000)
// [中文] 显式请求创建或继续响应,使用本段实际设置的模态、上下文与工具字段;发送请求不等同于收到完成事件。
// [中文] 本段为配置或调用片段,需结合外层程序与已建立连接使用;不作为独立完整程序宣称编译通过。
func handler(client openai.Client) http.Handler {
var mu sync.Mutex
type acceptance struct {
done chan struct{}
err error
}
calls := make(map[string]*acceptance)
mux := http.NewServeMux()
mux.HandleFunc("POST /webhook", func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
if err != nil {
http.Error(w, "Invalid webhook body", 400)
return
}
event, err := client.Webhooks.Unwrap(body, r.Header)
if err != nil {
http.Error(w, "Invalid webhook signature", 400)
return
}
if event.Type != "realtime.call.incoming" {
w.WriteHeader(200)
return
}
callID := event.Data.CallID
mu.Lock()
attempt, exists := calls[callID]
if !exists {
attempt = &acceptance{done: make(chan struct{})}
calls[callID] = attempt
}
mu.Unlock()
if exists {
select {
case <-attempt.done:
case <-r.Context().Done():
http.Error(w, "Request canceled", http.StatusRequestTimeout)
return
}
} else {
// Share this attempt's result only with retries of the same call.
// Persist accepted call IDs in production for deduplication across restarts.
attempt.err = client.Realtime.Calls.Accept(r.Context(), callID, realtime.CallAcceptParams{
RealtimeSessionCreateRequest: realtime.RealtimeSessionCreateRequestParam{
Model: "gpt-realtime-2.1",
Instructions: openai.String("You are a helpful support agent."),
},
})
mu.Lock()
if attempt.err != nil {
delete(calls, callID) // Allow a later delivery to retry a failed attempt.
}
close(attempt.done)
mu.Unlock()
if attempt.err == nil {
go func() {
if err := greetCaller(client, callID); err != nil {
log.Print("Call greeting failed: ", err)
}
}()
}
}
if attempt.err != nil {
http.Error(w, "Could not accept call", 502)
return
}
w.WriteHeader(200)
})
return mux
}
func greetCaller(client openai.Client, callID string) error {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
conn, err := client.Realtime.Connect(ctx, realtime.RealtimeConnectionOptions{}, option.WithQuery("call_id", callID))
if err != nil {
return err
}
defer conn.Close()
if err := conn.Send(ctx, realtime.RealtimeClientEventUnionParam{
OfResponseCreate: &realtime.ResponseCreateEventParam{
Response: realtime.RealtimeResponseCreateParams{
Instructions: openai.String("Thank the caller and ask how you can help."),
OutputModalities: []string{"audio"},
},
},
}); err != nil {
return err
}
for {
event, err := conn.Recv(ctx)
if err != nil {
return err
}
if event.Type == "error" {
return fmt.Errorf("Realtime API error: %s", event.OfRealtimeErrorEvent.Error.Message)
}
if event.Type == "response.done" {
if event.OfResponseDoneEvent.Response.Status != "completed" {
return fmt.Errorf("greeting did not complete")
}
return nil
}
}
}
来源示例 11(ruby):
沿用程序文字译释233。
程序文字译释234:你是乐于助人的客服人员。
程序文字译释235:感谢来电者,并询问需要什么帮助。
程序文字译释236:显示收到的Realtime事件类型。
## [中文] 示例 11:服务端示例;以下保留本语言原始示例。
## [中文] 原代码明确设置的模型值:gpt-realtime-2.1;各值的作用按其所在配置对象区分。
## [中文] 沿原示例执行这些方法:client.webhooks.unwrap、client.realtime.calls.accept、client.realtime.connect_to_call、connection.response.create;异步调用及事件接收顺序保留。
require "openai"
require "webrick"
client = OpenAI::Client.new(webhook_secret: ENV.fetch("OPENAI_WEBHOOK_SECRET"))
server = WEBrick::HTTPServer.new(
BindAddress: "127.0.0.1",
Port: Integer(ENV.fetch("OPENAI_WEBHOOK_PORT", "8000")),
Logger: WEBrick::Log.new($stderr, WEBrick::BasicLog::WARN),
AccessLog: []
)
sideband_workers = []
server.mount_proc("/webhook") do |request, response|
if request.request_method != "POST"
response.status = 405
next
end
headers = request.header.transform_values(&:first)
event = client.webhooks.unwrap(request.body, headers)
if event.is_a?(OpenAI::Models::Webhooks::RealtimeCallIncomingWebhookEvent)
call_id = event.data.call_id
sideband_workers.select!(&:alive?)
sideband_workers << Thread.new(call_id) do |active_call_id|
client.realtime.calls.accept(
active_call_id,
type: :realtime,
model: "gpt-realtime-2.1",
instructions: "You are a helpful support agent."
)
client.realtime.connect_to_call(call_id: active_call_id) do |connection|
connection.response.create(
instructions: "Thank the caller and ask how you can help."
)
connection.each do |server_event|
puts "Realtime event: #{server_event.type}"
end
end
end
end
response.status = 200
response.body = "ok"
rescue OpenAI::Errors::InvalidWebhookSignatureError, ArgumentError
response.status = 400
response.body = "Invalid signature"
ensure
server.shutdown if ENV["OPENAI_WEBHOOK_EXIT_AFTER_REQUEST"] == "1"
end
Signal.trap("INT") do
sideband_workers.each(&:kill)
server.shutdown
end
port = server.listeners.first.addr[1]
puts "Webhook server listening on http://127.0.0.1:#{port}/webhook"
$stdout.flush
server.start
sideband_workers.each(&:join)
后续步骤
SIP 连接成功后,可使用以下指南继续构建:

TRAE-AI编程


更多建议: