자체 Agent 연동

직접 만든 agent를 번호에 바인딩해 통화 대화를 넘겨받는 chat wire 규격.

번호에 자체 Agent를 바인딩하면 gateway는 전화 회선 · 음성 인식 · 음성 합성만 맡고, 무슨 말을 할지는 여러분의 엔드포인트가 정합니다. 수신과 발신을 각각 다른 유형으로 붙일 수 있습니다.

Agent 유형 고르기

번호 관리 › 번호 › 인바운드 또는 아웃바운드탭의 “Agent 연결”에서 유형과 chat URL을 저장합니다.

agent_type무엇수신에 저장되는 값발신에 저장되는 값
webhook일반 webhookchat URLchat URL
openai_compatibleOpenAI 호환chat URLchat URL
n8nn8nchat URLchat URL
langgraphLangGraphchat URL · Assistant IDchat URL · Assistant ID
mastraMastrachat URLchat URL · Agent ID

chat URL은 https만 허용합니다.

수신에서는 mastra의 Agent ID가 저장되지 않습니다. 콘솔에 입력 칸이 보여도 저장 시 비워지고, 수신 통화는 항상 기본 agent(voiceAssistant)로 연결됩니다. 특정 agent를 지정하려면 chat URL을 그 agent로 향하게 하거나 발신 바인딩을 쓰세요. (Assistant ID를 저장하는 유형은 수신에서 langgraph 하나뿐입니다.)

유형을 고르지 않거나 chat URL이 비어 있으면 발신은 아예 전화를 걸지 않고 422 no_agent_endpoint로 거절합니다. 수신은 통화를 거절합니다.

연결 규격

아래는 여러분이 호출하는 API가 아닙니다. 통화 중에 gateway가 여러분의 chat URL로 보내는 요청의 모양입니다 — 여러분 서버는 이걸 받아서 파싱하고 답을 돌려주면 됩니다.

수신·발신이 같은 어댑터 wire를 씁니다 — 한 번 구현하면 양방향에 그대로 씁니다.

일반 webhook

gateway가 chat URL로 POST {call_id, session_id, message, locale}를 보내고 {reply}를 받습니다.

gateway → chat URL — 여러분이 받는 요청
POST {chat_url}
Content-Type: application/json
{"call_id":"…","session_id":"…","message":"…","locale":"ko","vox":{…}}
→ {"reply":"…"}

통화 컨텍스트는 요청 본문 최상위 vox에서 읽습니다 — const { call_id, from, language } = req.body.vox;

OpenAI 호환

gateway가 {chat_url}/chat/completions로 messages 배열을 누적 전송합니다.

gateway → /chat/completions — 여러분이 받는 요청
POST {chat_url}/chat/completions
Authorization: Bearer <session_token>
X-Vox-Call-Id: …
{"messages":[…],"stream":true}

통화 컨텍스트는 X-Vox-* 요청 헤더 (본문은 그대로)에서 읽습니다 — const callId = req.headers['x-vox-call-id'];

n8n

n8n Chat Trigger의 webhook URL을 chat URL로 등록합니다. 응답은 output · text · message 필드에서 추출됩니다.

gateway → n8n webhook — 여러분이 받는 요청
POST {chat_url}
{"action":"sendMessage","sessionId":"<call_id>","chatInput":"…","metadata":{"vox":{…}}}
→ {"output":"…"}

통화 컨텍스트는 metadata.vox에서 읽습니다 — {{ $json.metadata.vox.call_id }}

LangGraph

chat URL에 LangGraph 서버 베이스(http://host:2024)를, Agent ID에 그래프 이름을 입력하세요. 통화 시작에 thread(= call_id)를 만들고 SSE로 스트리밍됩니다.

gateway → LangGraph runs/stream — 여러분이 받는 요청
POST {chat_url}/threads
{"thread_id":"<call_id>","if_exists":"do_nothing"}

POST {chat_url}/threads/{call_id}/runs/stream
{"assistant_id":"<그래프명>","input":{"messages":[…]},"stream_mode":"messages","config":{"configurable":{"vox":{…}}}}

통화 컨텍스트는 config.configurable.vox에서 읽습니다 — def node(state, config): vox = config['configurable']['vox']

Mastra

chat URL에 Mastra 서버 베이스(http://host:4111)를, Agent ID에 등록한 agent 이름을 입력하세요. SSE로 스트리밍됩니다.

gateway → Mastra /stream — 여러분이 받는 요청
POST {chat_url}/api/agents/{agentId}/stream
Authorization: Bearer <session_token>
{"messages":[…],"requestContext":{"vox":{…}}}

통화 컨텍스트는 requestContext.vox에서 읽습니다 — instructions: async ({ requestContext }) => requestContext.get('vox')

아웃바운드 kickoff

발신 통화에서는 상대가 받는 즉시 gateway가 아래 형식의 첫 user 턴(kickoff)을 보냅니다. 상대는 아직 아무 말도 하지 않았으므로, 여러분의 agent가 이 턴을 받아 먼저 말해야 합니다.

첫 user 턴 (kickoff)
[통화 시작 — 발신] 당신이 먼저 말합니다. 상대는 아직 아무 말도 하지 않았습니다.
[임무] {instruction}
  • 첫 user 턴이 kickoff입니다 — 상대는 아직 아무 말도 하지 않았고, 당신의 agent가 이 턴을 받아 먼저 말해야 합니다.
  • 임무(instruction)는 kickoff 턴으로만 전달됩니다 — 자연어 지시가 오는 채널은 이 턴 하나뿐입니다.
  • 통화 식별 정보(call_id·번호·언어)는 매 요청에 함께 옵니다 — 아래 '통화 컨텍스트' 참고. kickoff 턴에도 이미 실려 있으므로 첫 마디를 만들 때부터 쓸 수 있습니다.
  • 발신은 REST API(POST /api/v1/calls)를 사용하세요 — 착신번호(to)만 필수이고 instruction은 선택입니다 (agent가 이미 임무를 알고 있는 경우 생략).
  • 무음·최대 통화시간은 플랫폼 정책이지만, 통화 종료는 agent가 할 수 있습니다 — 아래 '통화 종료 (end_call)' 참고.
  • 인증: gateway가 모든 요청에 Authorization: Bearer <에이전트 인증 토큰>을 부착합니다. 토큰은 번호 설정에서 저장·발급합니다.

자연어 지시가 오는 채널은 kickoff 턴 하나뿐입니다 — 통화별로 달라지는 지시는 발신 요청의 instruction에 담으세요. 통화 식별 정보는 아래 “통화 컨텍스트”가 따로 나릅니다.

통화 컨텍스트

여러분의 agent가 지금 어느 통화에 붙어 있는지 알려주는 값입니다. 이 값의 call_id로 통화 조회·전사본·녹음·종료 API를 부를 수 있습니다.

수신(inbound)발신(outbound)
call_id통화 id통화 id
direction"inbound""outbound"
from발신자 번호내 번호
to내 번호착신자 번호
language통화 언어통화 언어

읽는 자리는 유형마다 다릅니다 — 위 “연결 규격”의 각 유형 설명을 보세요. 네 유형은 요청 본문의 vox 객체로 오고, openai_compatible요청 헤더로 옵니다(그 형식은 본문에 임의 키를 넣으면 거절하는 서버가 있어 본문을 건드리지 않습니다).

openai_compatible — 요청 헤더
X-Vox-Call-Id: …
X-Vox-Direction: …
X-Vox-From: …
X-Vox-To: …
X-Vox-Language: …
  • 수신·발신 양방향, 유형 5종 모두에 전달됩니다. 읽지 않아도 됩니다 — 기존 연결은 그대로 동작합니다.
  • 매 요청에 다시 실립니다. 세션 시작 한 번이 아니므로, 엔드포인트가 상태를 저장해 둘 필요가 없습니다.
  • 통화가 끝날 때까지 값이 바뀌지 않습니다 — 통화 시작 시점에 확정됩니다.
  • from·to의 의미는 방향에 따라 뒤집힙니다. 상대방 번호는 수신이면 from, 발신이면 to입니다.
  • 번호는 국내형 표기입니다(선행 0 유지, 하이픈 없음).
  • 자연어(임무·지침)는 여기 실리지 않습니다 — 식별 정보만 담는 자리입니다.

요청 인증

gateway가 여러분의 엔드포인트로 보내는 요청에 Authorization 헤더를 붙일 수 있습니다. 다만 그 값의 출처가 방향마다 다릅니다 — 저장해 두는 토큰은 아웃바운드에만 있습니다.

POST {chat_url}
Authorization: Bearer {agent_token}
Content-Type: application/json
아웃바운드 (발신)인바운드 (수신)
값의 출처번호에 저장한 에이전트 인증 토큰발신자 인증(handshake)이 돌려준 세션 토큰
설정하는 곳번호 관리 › 번호 › 아웃바운드 › 아웃바운드 Agent번호 관리 › 번호 › 인바운드 › 인바운드 인증
헤더가 안 붙는 경우토큰을 비워 뒀을 때인증 방식이 handshake가 아닐 때

아웃바운드 토큰은 암호화 저장되며 다시 조회되지 않습니다(존재 여부만 표시). 엔드포인트 쪽에서는 이 값이 저장한 토큰과 같은지 확인하면 됩니다.

인바운드에는 토큰을 저장하는 칸이 없습니다. 수신 통화에 인증 헤더를 받고 싶다면 인증 방식을 handshake로 두고 그 인증 API가 세션 토큰을 내려주게 하세요 — 발신자마다 다른 값을 줄 수 있다는 것이 이 구조의 이유입니다.

통화 종료 (hangup)

대화가 끝났다고 판단하면 agent가 직접 통화를 끊을 수 있습니다. 표면은 REST 하나입니다.

POST/api/v1/calls/{call_id}/hangup
curl -X POST https://vox-gateway-prod.fly.dev/api/v1/calls/{call_id}/hangup \
  -H "Authorization: Bearer $VOX_API_KEY"
# → 202 {"call_id": "...", "status": "hangup_requested"}
  • 이 호출은 종료 “요청”입니다(응답 202) — 실제 종료 확정은 이벤트 Webhook의 call.ended로 관찰하세요. 사유는 end_reason=agent_hangup(이력엔 “AI 종료”).
  • 즉시 끊기지 않습니다 — 작별 인사가 끝나면 끊깁니다.
  • 인사 도중 상대가 다시 말하면 종료가 자동 취소되고 통화가 이어집니다 — 아직 할 말이 남은 상대를 끊지 않으려는 안전장치입니다.
  • 작별 인사를 말한 뒤(또는 말하면서) 호출하세요 — 먼저 호출하고 침묵하면 인사가 안 들립니다.
  • 거절돼도 통화를 끊지 마세요 — 사유를 받고 대화를 이어갑니다.
HTTPerror
409call_not_active통화가 진행 중이 아님 — 재시도 무의미.
409room_unknown종료 신호를 보낼 방(room)을 못 찾음 — 재시도 무의미.
404call_not_found내 통화가 아님(존재 여부도 감춤).
502hangup_failed신호 전송 일시 실패 — 한 번 재시도 가능.
503hangup_unavailablegateway에 LiveKit 미설정 — 기능 off.

거절돼도 통화는 살아 있습니다 — 사유를 받고 대화를 이어가세요.

전체 요청·응답 규격은 Calls API를 보세요.