← 블로그 목록
가이드2026-06-01

070 인바운드 webhook 라우팅 — 시간·번호·DTMF 기준 동적 분기

070 인바운드 webhook 라우팅 — 시간·번호·DTMF 기준 동적 분기

같은 070 번호로 들어오는 통화도 시간대(낮/밤)·발신자(VIP/일반)·DTMF 응답(1=예약/2=문의/3=결제) 기준으로 다른 시나리오로 분기해야 한다. ClawOps webhook 으로 통화 시작 직전에 라우팅 결정.

0. 사전 준비

  • ClawOps 070 발급 완료
  • HTTPS 가능한 webhook endpoint (Vercel/Fly/Render OK)

1. webhook 등록

client.numbers.update(
    number_id="num_abc",
    webhook_url="https://your-server.com/voice-route",
    webhook_events=["call.initiated", "call.dtmf", "call.ended"],
)

또는 Console UI 에서:

Number → Settings → Webhook URL → https://your-server.com/voice-route

2. webhook 페이로드 (call.initiated)

ClawOps 가 통화 시작시 다음 형태로 POST:

{
  "event": "call.initiated",
  "call": {
    "id": "call_xyz",
    "from": "01012345678",
    "to": "07052358010",
    "started_at": "2026-05-27T10:30:00Z",
    "metadata": {}
  }
}

당신의 endpoint 는 라우팅 결정을 JSON 으로 응답:

{
  "action": "ai_handle",
  "agent_config": {
    "system_prompt": "안녕하세요, ABC 회사입니다...",
    "language": "ko-KR",
    "voice": "alloy"
  }
}

3. 라우팅 패턴들

A. 영업시간 기준 분기

from flask import Flask, request, jsonify
import datetime
from zoneinfo import ZoneInfo

app = Flask(__name__)

@app.route("/voice-route", methods=["POST"])
def voice_route():
    payload = request.json
    now = datetime.datetime.now(ZoneInfo("Asia/Seoul"))
    is_business_hours = 9 <= now.hour < 18 and now.weekday() < 5

    if is_business_hours:
        return jsonify({
            "action": "ai_handle",
            "agent_config": {
                "system_prompt": "안녕하세요, ABC 회사입니다. 어떤 도움이 필요하신가요?",
                "language": "ko-KR",
            },
        })
    else:
        return jsonify({
            "action": "ai_handle",
            "agent_config": {
                "system_prompt": (
                    "안녕하세요, ABC 회사 야간 응대입니다. "
                    "지금은 영업시간이 아니라 메모만 받겠습니다. "
                    "성함과 용건 말씀해주시면 영업일 첫 시간에 연락드립니다."
                ),
                "language": "ko-KR",
                "max_duration_seconds": 120,
            },
        })

B. 발신자 번호 화이트리스트 (VIP)

VIP_NUMBERS = {"01011112222", "01033334444"}

@app.route("/voice-route", methods=["POST"])
def voice_route():
    caller = request.json["call"]["from"]
    if caller in VIP_NUMBERS:
        return jsonify({
            "action": "transfer",
            "to": "010-VIP담당자-번호",
            "mode": "blind",
        })
    return jsonify({"action": "ai_handle", "agent_config": {...}})

C. DTMF 응답 기반 메뉴

@app.route("/voice-route", methods=["POST"])
def voice_route():
    # 첫 응답 — 메뉴 안내
    return jsonify({
        "action": "ai_handle",
        "agent_config": {
            "system_prompt": (
                "다음 중 선택해주세요. "
                "예약은 1번, 결제 문의는 2번, 상담사 연결은 0번을 눌러주세요."
            ),
            "language": "ko-KR",
            "expect_dtmf": True,                 # DTMF 이벤트 emit
        },
    })

@app.route("/voice-dtmf", methods=["POST"])     # call.dtmf 이벤트 endpoint
def voice_dtmf():
    digit = request.json["dtmf"]["digit"]
    call_id = request.json["call"]["id"]

    if digit == "1":
        return jsonify({
            "action": "switch_agent",
            "agent_config": {
                "system_prompt": "예약 안내 시작 — 날짜와 인원을 알려주세요...",
            },
        })
    elif digit == "2":
        return jsonify({
            "action": "switch_agent",
            "agent_config": {
                "system_prompt": "결제 문의 안내 — 주문번호 알려주세요...",
            },
        })
    elif digit == "0":
        return jsonify({
            "action": "transfer",
            "to": "010-상담사-번호",
            "mode": "warm",
            "context": "고객이 메뉴에서 상담사 연결 선택",
        })

D. 발신자 history 기반 (DB 조회)

@app.route("/voice-route", methods=["POST"])
def voice_route():
    caller = request.json["call"]["from"]
    customer = db.query("SELECT * FROM customers WHERE phone = %s", caller).fetchone()

    if customer is None:
        prompt = "안녕하세요, 처음 전화 주신 분이시군요. ABC 회사입니다."
    elif customer.tier == "premium":
        prompt = f"{customer.name}님 안녕하세요. 프리미엄 고객 전용 응대입니다."
    elif customer.unpaid_invoices > 0:
        prompt = f"{customer.name}님, 미결제 청구건이 있어서 안내드립니다..."
    else:
        prompt = f"{customer.name}님 안녕하세요. 어떤 도움이 필요하신가요?"

    return jsonify({
        "action": "ai_handle",
        "agent_config": {"system_prompt": prompt, "language": "ko-KR"},
    })

E. 부재중 fallback (영업시간 외 / AI 처리 실패)

@app.route("/voice-route", methods=["POST"])
def voice_route():
    # 영업시간 외 → 메시지 받기 모드
    return jsonify({
        "action": "ai_handle",
        "agent_config": {
            "system_prompt": (
                "지금은 영업시간이 아닙니다. "
                "성함, 연락처, 용건을 차례로 말씀해주시면 다음 영업일에 callback 드립니다."
            ),
            "tools": ["hang_up"],                # AI 가 끝나면 자동 종료
            "post_call_actions": [               # 끝난 뒤 자동 실행
                {"type": "summarize"},
                {"type": "webhook", "url": "https://your-server.com/voicemail"},
            ],
        },
    })

4. 작동 원리 — 분기 흐름

[고객] → 070 다이얼
   ↓
[ClawOps SIP 수신]
   ↓ POST /voice-route
[당신의 endpoint] 라우팅 결정
   ↓ JSON 응답
[ClawOps Agent 시작] OR [Transfer] OR [Voicemail]
   ↓
[고객]

5. 성능 — endpoint 응답 시간

ClawOps 는 webhook 응답을 통화 시작 전 200ms 안에 기대. endpoint 가 늦으면 default agent_config 로 fallback.

  • DB 조회 1개 (<50ms) OK
  • 외부 API 호출은 비동기 큐로

6. 보안

  • ClawOps webhook 은 X-ClawOps-Signature 헤더에 HMAC-SHA256 서명 보냄
  • 검증:
import hmac, hashlib
def verify(body: bytes, sig: str) -> bool:
    expected = hmac.new(WEBHOOK_SECRET.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)

다음 단계

관련 글 더 보기

ClawOps AI 전화 API로 시작하기

070 번호 발급부터 AI 음성 통화까지, REST API 몇 줄이면 됩니다.