OpenAI 电话与 SIP

2026-11-06 12:40 更新

选择所用API,查看对应的连接步骤与事件。

通过SIP中继把呼入和呼出电话连接到GPT-Live。提供商与OpenAI交换音频,后端处理会话配置、通话控制和业务逻辑。SIP信令使用TLS,GPT-Live要求通话音频使用SRTP。

  • 呼入:接收来电webhook,再接受或拒绝。
  • 呼出:创建会话,让提供商拨打电话号码,不需要来电webhook或accept请求。

后端需要接收会话事件或发送命令时,使用旁路连接附加到既有对话,音频仍由SIP承载。每项动作只分配一个处理器,避免重复webhook或多连接观察到同一事件时执行两次工具。

处理呼入

把SIP提供商的来电路由至OpenAI项目,后端接收webhook并决定是否接受。

配置电话号码与中继

使用Twilio等SIP中继提供商。向提供商购买号码后:

  1. 打开项目设置,在Webhooks下创建订阅live.transport.incoming的webhook端点。
  2. 在General中取得以proj_开头的项目ID,必须与webhook所属项目一致。
  3. 将提供商中继指向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后续步骤

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/28
  • 23.98.140.64/28
  • 40.67.149.176/28
  • 40.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 连接成功后,可使用以下指南继续构建:

其他资源

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号