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(
"07052358010",
webhook_url="https://your-server.com/voice-route",
)
또는 Console UI 에서:
Number → Settings → Webhook URL → https://your-server.com/voice-route
2. webhook 페이로드 (인바운드 콜)
ClawOps 가 수신전화 시 application/x-www-form-urlencoded 로 POST 한다. 주요 파라미터:
POST /voice-route
Content-Type: application/x-www-form-urlencoded
X-Signature: <서명키 등록 시 포함>
CallId=CA1a2b3c...&AccountId=AC...&From=01012345678&To=07052358010&CallStatus=in-progress&Direction=inbound
| 파라미터 | 예시 | 설명 |
|---|---|---|
CallId | CA1a2b... | 통화 고유 ID |
AccountId | AC... | 계정 ID |
From | 01012345678 | 발신 번호 |
To | 07052358010 | 수신 ClawOps 070 |
CallStatus | in-progress | 통화 상태 |
Direction | inbound | 방향 |
당신의 endpoint 는 Content-Type: application/xml 로 VoiceML XML 을 응답한다. 루트는 <Response>. AI 로 응대하려면 <Connect><Stream> 으로 실시간 양방향 오디오를 당신의 WebSocket(STT/LLM/TTS)에 연결한다:
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say language="ko">안녕하세요, ABC 회사입니다.</Say>
<Connect>
<Stream url="wss://your-server.com/media"/>
</Connect>
</Response>
3. 라우팅 패턴들
A. 영업시간 기준 분기
영업중에는 <Connect><Stream> 으로 AI 가 받고, 야간에는 <Say> 안내 후 <Record> 로 메모를 받는다.
from flask import Flask, request, Response
import datetime
from zoneinfo import ZoneInfo
app = Flask(__name__)
@app.route("/voice-route", methods=["POST"])
def voice_route():
now = datetime.datetime.now(ZoneInfo("Asia/Seoul"))
is_business_hours = 9 <= now.hour < 18 and now.weekday() < 5
if is_business_hours:
# 영업중 — AI 가 실시간 응대 (WebSocket 으로 STT/LLM/TTS)
xml = """<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say language="ko">안녕하세요, ABC 회사입니다. 어떤 도움이 필요하신가요?</Say>
<Connect>
<Stream url="wss://your-server.com/media"/>
</Connect>
</Response>"""
else:
# 야간 — 안내 후 메모 녹음
xml = """<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say language="ko">안녕하세요, ABC 회사 야간 응대입니다. 지금은 영업시간이 아니라 메모만 받겠습니다. 성함과 용건을 말씀해주시면 영업일 첫 시간에 연락드립니다.</Say>
<Record maxLength="120" playBeep="true" action="https://your-server.com/voicemail"/>
</Response>"""
return Response(xml, mimetype="application/xml")
B. 발신자 번호 화이트리스트 (VIP)
VIP 발신자는 <Dial><Number> 로 담당자에게 바로 연결, 그 외는 AI 응대.
VIP_NUMBERS = {"01011112222", "01033334444"}
@app.route("/voice-route", methods=["POST"])
def voice_route():
caller = request.form["From"]
if caller in VIP_NUMBERS:
# VIP 담당자에게 전환
xml = """<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Dial timeout="20" action="https://your-server.com/dial-status">
<Number>07012340002</Number>
</Dial>
</Response>"""
else:
# 일반 — AI 응대
xml = """<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say language="ko">안녕하세요, ABC 회사입니다.</Say>
<Connect>
<Stream url="wss://your-server.com/media"/>
</Connect>
</Response>"""
return Response(xml, mimetype="application/xml")
C. DTMF 응답 기반 메뉴
<Gather> 로 DTMF 를 수집하고, action URL 이 받는 Digits 파라미터로 분기한다.
@app.route("/voice-route", methods=["POST"])
def voice_route():
# 첫 응답 — 메뉴 안내 + DTMF 수집
xml = """<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Gather numDigits="1" timeout="5" action="https://your-server.com/voice-dtmf">
<Say language="ko">예약은 1번, 결제 문의는 2번, 상담사 연결은 0번을 눌러주세요.</Say>
</Gather>
</Response>"""
return Response(xml, mimetype="application/xml")
@app.route("/voice-dtmf", methods=["POST"]) # Gather action — Digits 파라미터로 받음
def voice_dtmf():
digit = request.form.get("Digits", "")
if digit == "1":
xml = """<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say language="ko">예약 안내를 시작합니다. 날짜와 인원을 말씀해주세요.</Say>
<Connect>
<Stream url="wss://your-server.com/media?flow=reservation"/>
</Connect>
</Response>"""
elif digit == "2":
xml = """<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say language="ko">결제 문의 안내입니다. 주문번호를 말씀해주세요.</Say>
<Connect>
<Stream url="wss://your-server.com/media?flow=payment"/>
</Connect>
</Response>"""
else: # "0" — 상담사 연결
xml = """<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say language="ko">상담사에게 연결해드리겠습니다.</Say>
<Dial timeout="20" action="https://your-server.com/dial-status">
<Number>07012340001</Number>
</Dial>
</Response>"""
return Response(xml, mimetype="application/xml")
D. 발신자 history 기반 (DB 조회)
발신자 history 로 첫 안내 멘트(<Say>)를 바꾸고 AI 로 연결한다. (AI 응대 컨텍스트는 <Stream> URL 쿼리로 WebSocket 핸들러에 전달.)
from xml.sax.saxutils import escape
@app.route("/voice-route", methods=["POST"])
def voice_route():
caller = request.form["From"]
customer = db.query("SELECT * FROM customers WHERE phone = %s", caller).fetchone()
if customer is None:
greeting = "안녕하세요, 처음 전화 주신 분이시군요. ABC 회사입니다."
ctx = "new"
elif customer.tier == "premium":
greeting = f"{customer.name}님 안녕하세요. 프리미엄 고객 전용 응대입니다."
ctx = "premium"
elif customer.unpaid_invoices > 0:
greeting = f"{customer.name}님, 미결제 청구건이 있어서 안내드립니다."
ctx = "unpaid"
else:
greeting = f"{customer.name}님 안녕하세요. 어떤 도움이 필요하신가요?"
ctx = "returning"
xml = f"""<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say language="ko">{escape(greeting)}</Say>
<Connect>
<Stream url="wss://your-server.com/media?ctx={ctx}"/>
</Connect>
</Response>"""
return Response(xml, mimetype="application/xml")
E. 부재중 fallback (영업시간 외 / AI 처리 실패)
영업시간 외에는 안내 후 <Record> 로 음성 메모를 받는다. 녹음이 끝나면 action URL 로 RecordingUrl/RecordingDuration 이 POST 되니, 거기서 요약·callback 큐 적재를 한다.
@app.route("/voice-route", methods=["POST"])
def voice_route():
# 영업시간 외 → 메시지 받기 모드
xml = """<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say language="ko">지금은 영업시간이 아닙니다. 성함, 연락처, 용건을 차례로 말씀해주시면 다음 영업일에 콜백 드립니다.</Say>
<Record maxLength="60" playBeep="true" action="https://your-server.com/voicemail"/>
</Response>"""
return Response(xml, mimetype="application/xml")
@app.route("/voicemail", methods=["POST"]) # Record action — 녹음 완료 후 호출
def voicemail():
recording_url = request.form.get("RecordingUrl")
duration = request.form.get("RecordingDuration")
# 여기서 STT 요약 → callback 큐 적재 등 후처리
save_voicemail(recording_url, duration)
xml = """<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say language="ko">메모 잘 받았습니다. 다음 영업일에 연락드리겠습니다. 감사합니다.</Say>
<Hangup/>
</Response>"""
return Response(xml, mimetype="application/xml")
4. 작동 원리 — 분기 흐름
[고객] → 070 다이얼
↓
[ClawOps SIP 수신]
↓ POST /voice-route
[당신의 endpoint] 라우팅 결정
↓ VoiceML(XML) 응답
[AI 응대 <Connect><Stream>] OR [<Dial> 전환] OR [<Record> 메모]
↓
[고객]
5. 성능 — endpoint 응답 시간
ClawOps 는 webhook 응답을 통화 시작 전 200ms 안에 기대. endpoint 가 늦으면 통화가 지연되니 빠르게 VoiceML 을 반환해야 한다.
- DB 조회 1개 (<50ms) OK
- 외부 API 호출은 비동기 큐로
6. 보안
- 서명키를 등록하면 ClawOps webhook 요청에
X-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)
다음 단계
관련 글 더 보기
ngrok 없이 로컬에서 음성 에이전트 띄우기 — 5분, ClawOps managed inbound
ngrok·터널링·자체 webhook 서버 없이 로컬 머신에서 한국어 voice agent 띄우는 가이드. ClawOps managed inbound 패턴 + Python 30줄.
가이드WebRTC 에서 한국 070 통화하기 — 브라우저 한 줄, Asterisk 없이
브라우저 WebRTC 에서 직접 한국 070 번호로 통화 거는 가이드. Asterisk·FreeSWITCH 자체 운영 없이 ClawOps WebRTC SDK 1줄.
가이드관리번호로 070 대량 발급·관리하기 — 파트너 가입중개(External Assignment)
프랜차이즈·플랫폼·법인이 최종이용자에게 070을 일괄 발급·관리하는 가이드. ClawOps 관리번호 Add-on + 발급 링크·본인인증 + 인입 라우팅 코드.
가이드070 번호 5분 발급 가이드 — API 한 줄, 무약정, 카드 등록 없이
한국 070 번호를 API 한 줄로 5분에 발급받는 가이드. 통신사 영업·KISA 등록·벌크 약정 없이 ClawOps Trial 무료로 시작.
가이드1588·1566 대표번호 ClawOps 무료 연동 — 기존 8자리 번호 그대로 AI 응답
기존 1588·1566 대표번호를 ClawOps 070 인프라에 무료로 연동해서 AI 상담원이 받게 만드는 가이드. 기존 통신사 계약 유지하면서 AI 응대 추가.