OpenAI Realtime
한국 070 번호 인바운드를 OpenAI Realtime API (gpt-realtime) 의 SIP 게이트웨이로 직접 전달하는 가이드. webhook 핸들러 코드 포함.
개요
ClawOps 070 번호를 OpenAI Project 의 SIP 게이트웨이 (sip.api.openai.com) 로 라우팅합니다. 통화가 도착하면 OpenAI 가 realtime.call.incoming webhook 을 호출하고, 우리 서버가 accept 응답하면 OpenAI 가 음성 모델로 통화를 처리합니다.
SIP 트렁크 연결 부가서비스 가 활성화되어 있어야 합니다. 대시보드 → 부가서비스 에서 켜주세요.
동작 흐름
사전 준비물
| 항목 | 출처 |
|---|---|
Project ID (proj_xxx) | platform.openai.com → Settings → General |
API Key (sk-proj-...) | Settings → API keys |
Webhook Signing Secret (whsec_...) | Settings → Webhooks (3단계에서 생성) |
1단계 — Webhook 핸들러 서버
OpenAI 가 콜이 들어올 때 호출할 엔드포인트입니다. 서명 검증 후 accept 를 호출하고, WebSocket 으로 세션을 모니터링합니다.
package.json:
{
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts"
},
"dependencies": {
"body-parser": "^1.20.3",
"dotenv": "^16.4.5",
"express": "^4.21.2",
"openai": "^6.37.0",
"ws": "^8.18.0"
},
"devDependencies": {
"@types/body-parser": "^1.19.5",
"@types/express": "^4.17.21",
"@types/node": "^22.10.5",
"@types/ws": "^8.5.13",
"tsx": "^4.19.2",
"typescript": "^5.7.3"
}
}.env:
OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxx
OPENAI_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxx
PORT=8000src/index.ts:
import bodyParser from "body-parser"
import "dotenv/config"
import express, { Request, Response } from "express"
import OpenAI from "openai"
import WebSocket from "ws"
const PORT = Number(process.env.PORT ?? 8000)
const OPENAI_API_KEY = process.env.OPENAI_API_KEY!
const OPENAI_WEBHOOK_SECRET = process.env.OPENAI_WEBHOOK_SECRET!
const openai = new OpenAI({
apiKey: OPENAI_API_KEY,
webhookSecret: OPENAI_WEBHOOK_SECRET,
})
const AUTH_HEADERS = {
Authorization: `Bearer ${OPENAI_API_KEY}`,
"Content-Type": "application/json",
}
const callAccept = {
type: "realtime",
model: "gpt-realtime",
instructions: "당신은 친절한 한국어 음성 비서입니다.",
audio: { output: { voice: "alloy" } },
}
const responseCreate = {
type: "response.create",
response: { instructions: "사용자에게 '안녕하세요, 무엇을 도와드릴까요?' 라고 인사하세요." },
}
async function websocketTask(callId: string) {
const ws = new WebSocket(
`wss://api.openai.com/v1/realtime?call_id=${callId}`,
{ headers: { Authorization: `Bearer ${OPENAI_API_KEY}` } },
)
ws.on("open", () => ws.send(JSON.stringify(responseCreate)))
ws.on("message", (raw) => console.log(`[ws] ${raw.toString("utf8")}`))
ws.on("error", (e) => console.error("[ws error]", e))
}
const app = express()
app.use(bodyParser.raw({ type: "*/*" }))
app.post("/", async (req: Request, res: Response) => {
try {
const event = await openai.webhooks.unwrap(
(req.body as Buffer).toString("utf8"),
req.headers as Record<string, string>,
)
if (event.type === "realtime.call.incoming") {
const callId = (event as any).data.call_id
await fetch(
`https://api.openai.com/v1/realtime/calls/${callId}/accept`,
{ method: "POST", headers: AUTH_HEADERS, body: JSON.stringify(callAccept) },
)
websocketTask(callId).catch((e) => console.error("[ws] failed", e))
}
return res.sendStatus(200)
} catch (e: any) {
if (e?.name === "InvalidWebhookSignatureError") {
return res.status(400).send("invalid signature")
}
console.error("[webhook] error", e)
return res.status(500).send("server error")
}
})
app.listen(PORT, () => console.log(`Listening on http://localhost:${PORT}`))pnpm i
pnpm dev
# → Listening on http://localhost:80002단계 — 서버를 외부로 노출
OpenAI 가 webhook 을 호출할 수 있도록 외부 URL 로 노출합니다.
brew install cloudflared
cloudflared tunnel --url http://localhost:8000
# → https://xxxx-xxxx-xxxx.trycloudflare.com운영 환경은 고정 도메인 (cloudflared named tunnel, ngrok reserved domain 등) 사용을 권장합니다.
3단계 — OpenAI 콘솔에서 Webhook 등록
platform.openai.com → Settings → Webhooks → Create endpoint
- URL: 2단계에서 받은 URL (예:
https://xxxx.trycloudflare.com/) - Event types:
realtime.call.incoming
생성 직후 표시되는 Signing secret (whsec_...) 을 복사해 1단계 .env 의 OPENAI_WEBHOOK_SECRET 에 붙여넣은 후 서버를 재시작합니다.
4단계 — ClawOps 콘솔에서 SIP 엔드포인트 등록
platform.claw-ops.com/phone-numbers/sip-endpoints → 엔드포인트 추가
| 필드 | 값 |
|---|---|
| 엔드포인트 이름 | OpenAI Realtime (자유) |
| User | OpenAI Project ID (proj_xxx) |
| Host | sip.api.openai.com |
| Port | 5061 |
| Transport | TLS |
| 외부 SIP 인증 사용 | 체크 안 함 |
라이브 프리뷰가 sip:proj_xxx@sip.api.openai.com:5061 로 표시되면 정상입니다.
5단계 — 070 번호 라우팅을 SIP 로 전환
platform.claw-ops.com/phone-numbers → 번호 선택 → 인바운드 라우팅 카드의 수정 → SIP 토글 → 4단계의 엔드포인트 선택 → 저장.
이제 해당 번호로 걸려오는 콜은 OpenAI 의 SIP 게이트웨이로 전달되고, 1단계의 webhook 핸들러가 호출됩니다.
참고
- Realtime API with SIP — OpenAI 공식 가이드
- Realtime Calls API — accept / reject / refer / hangup 엔드포인트 레퍼런스