ClawOps Docs

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=8000

src/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:8000

2단계 — 서버를 외부로 노출

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단계 .envOPENAI_WEBHOOK_SECRET 에 붙여넣은 후 서버를 재시작합니다.


4단계 — ClawOps 콘솔에서 SIP 엔드포인트 등록

platform.claw-ops.com/phone-numbers/sip-endpoints엔드포인트 추가

필드
엔드포인트 이름OpenAI Realtime (자유)
UserOpenAI Project ID (proj_xxx)
Hostsip.api.openai.com
Port5061
TransportTLS
외부 SIP 인증 사용체크 안 함

라이브 프리뷰가 sip:proj_xxx@sip.api.openai.com:5061 로 표시되면 정상입니다.


5단계 — 070 번호 라우팅을 SIP 로 전환

platform.claw-ops.com/phone-numbers → 번호 선택 → 인바운드 라우팅 카드의 수정SIP 토글 → 4단계의 엔드포인트 선택 → 저장.

이제 해당 번호로 걸려오는 콜은 OpenAI 의 SIP 게이트웨이로 전달되고, 1단계의 webhook 핸들러가 호출됩니다.


참고