새로운 문서를 작성 중입니다.새 문서로 이동
ClawOps Docs

이벤트 & CallSession

이벤트 핸들러

agent.on() 메서드로 통화 이벤트를 수신합니다.

agent.on('call_start', async (call) => {
  console.log(`통화 시작: ${call.fromNumber} -> ${call.toNumber}`);
  console.log(`통화 ID: ${call.callId}`);
});

agent.on('call_end', async (call) => {
  console.log(`통화 종료: ${call.callId} (총 ${call.duration.toFixed(1)}초)`);
});

agent.on('transcript', async (call, role, text) => {
  console.log(`[${role}] ${text}`);
  // role: "user" (고객 음성 인식) 또는 "assistant" (AI 응답)
});

agent.on('call_failed', async (call, reason) => {
  console.log(`통화 미연결: ${reason}`);
  // reason: "no-answer" / "busy" / "rejected" / "canceled" / "failed"
});

이벤트 목록

이벤트파라미터설명
call_start(call)통화 시작 — 상대가 받은 뒤 미디어 세션이 열릴 때
call_end(call)통화 종료 — call_start 가 발화된 통화만
call_failed(call, reason)통화가 연결되지 못하고 종료됨. reason 은 종료 사유
transcript(call, role, text)음성 텍스트 생성
dtmf(call, digit)DTMF 키 입력 수신

call_start/call_end 는 통화가 응답된 뒤 열리는 미디어 세션에 묶여 있습니다. 상대가 받지 않았거나 (무응답) 통화중·거절이면 이 두 이벤트는 발화되지 않고, 대신 call_failed 가 발화됩니다. 즉 발신 한 건은 반드시 call_start+call_end 또는 call_failed 중 한쪽으로 끝납니다.

결과발화되는 이벤트
상대가 받고 통화 종료call_start → call_end
무응답 · 통화중 · 거절 · 취소call_failed
연결됐으나 시스템 오류로 종료call_start → call_end + call_failed(reason="failed")

reason 값은 call.endedStatus 와 동일합니다. 자세한 의미는 발신 결과 확인하기 를 참고하세요.

수신 통화 거절 — onIncoming

agent.onIncoming() 에 함수를 등록하면 수신 통화를 받기 전에 보고 거절할 수 있습니다. 거절한 통화는 받지 않은 채 끝나므로 과금되지 않고, 다른 경로로 넘어가지도 않습니다.

// 번호 표시제한 통화는 통화중으로 거절
agent.onIncoming(async (call) => {
  if (call.callerIdRestricted) await call.reject({ reason: 'busy' });
});

call 은 CallSession 이 아니라 아직 받지 않은 통화를 나타내는 IncomingCall 입니다.

속성·메서드타입설명
callIdstring통화 ID
fromNumberstring발신 번호
toNumberstring수신 번호
callerIdRestrictedboolean발신자가 번호 표시제한을 걸고 건 통화면 true (위 CallSession 과 같은 값)
reject(opts?)Promise<void>통화를 거절합니다. reason 은 'rejected'(기본) 또는 'busy'
reason발신자가 듣는 것
'rejected' (기본)수신 거절 (SIP 403)
'busy'통화중 (SIP 486)
  • reject() 를 부르지 않고 함수가 끝나면 받습니다. 이후는 평소와 같이 call_start 부터 이어집니다.
  • 거절한 통화는 세션이 만들어지지 않으므로 call_start·call_end·call_failed 가 발화되지 않습니다.
  • 함수가 5초 안에 끝나지 않거나 예외를 던지면 경고 로그를 남기고 받습니다. 선별 함수의 실수로 통화를 잃지 않기 위해서입니다. 외부 조회를 한다면 5초보다 짧은 시간 제한을 직접 거세요.
  • 5초가 지난 뒤 reject() 를 부르면 이미 받은 통화라 아무 일도 일어나지 않습니다(경고 로그만 남습니다).
  • reject() 를 두 번 불러도 한 번만 처리됩니다. 'rejected'·'busy' 가 아닌 reason 을 넘기면 예외가 납니다.
  • onIncoming() 을 다시 부르면 이전 함수를 바꿉니다. 등록하지 않으면 모든 수신 통화를 받습니다.

CallSession

개별 통화의 상태를 관리합니다. 이벤트 핸들러의 call 파라미터로 전달됩니다.

속성

속성타입설명
callIdstring통화 ID
fromNumberstring발신 번호
toNumberstring수신 번호
accountIdstring계정 ID
directionstring"inbound" 또는 "outbound"
callerIdRestrictedboolean수신 통화에서 발신자가 번호 표시제한(*23# 등)을 걸고 건 통화면 true. 번호는 fromNumber 에 그대로 전달됩니다. 통신사가 표시제한 여부를 보내지 않은 통화와 발신 통화는 false
statusCallStatus수명주기 상태. 아래 표 참고
endedStatusstring | null종료 사유. 통화가 끝나기 전에는 null
endedDurationnumber | null서버가 확정한 통화 시간(초). 아래 설명 참고
startTimeDate통화 시작 시간
durationnumberSDK 가 로컬 시계로 재는 경과 시간 (초). 통화 중에도 읽힙니다
metadataRecord<string, unknown>사용자 정의 메타데이터

duration 과 endedDuration

의미언제 읽나
durationSDK 가 로컬 시계로 재는 경과 시간통화 중에도 읽힙니다
endedDuration서버가 확정한 통화 시간통화가 끝난 뒤

기록·정산에는 endedDuration 을 쓰세요. duration 은 세션이 붙기 전후의 오차를 포함합니다.

보통은 call_end 핸들러 안에서 바로 읽을 수 있습니다. 서버는 미디어 스트림을 먼저 닫고 정리를 마친 뒤에 종료 정보를 보내므로, SDK 가 그 값을 짧게 기다렸다가 call_end 를 발화합니다.

전환(transfer)으로 끝나는 통화는 예외입니다. 전환이 시작되면 AI 의 미디어 세션이 먼저 끝나므로 call_end 가 그 시점에 발화하는데, 통화 자체는 담당자와 계속 이어지고 있습니다. 그래서 그 순간에는 전체 통화 시간이 아직 정해지지 않았고 endedDuration 은 null 입니다. 전환 구간의 길이는 transfer() 의 반환값(duration)으로 받으시고, 전체 통화 시간이 필요하면 통화 종료 webhook(statusCallback) 이나 통화 조회 API 를 쓰세요.

status — 수명주기

값시점
ringing세션 생성 직후 (발신·수신 공통)
active통화가 응답되어 미디어 세션이 열릴 때
ended통화가 끝났을 때 — 응답 여부와 무관

endedStatus — 종료 사유

값의미
completed상대가 받았고 통화가 정상 종료됨
no-answer벨은 울렸으나 받지 않음 (timeout 초과로 발신 취소)
busy통화중
rejected상대가 거절
canceled상대가 받기 전에 발신 측이 취소
failed시스템/네트워크 오류

status 는 상대가 받았든 아니든 ended 가 되므로, 통화 성사 여부는 endedStatus 로 판단합니다. completed 만이 실제로 연결된 통화를 의미합니다.

const session = await agent.call('01012345678');
await session.wait();
if (session.endedStatus !== 'completed') {
  console.log(`통화 미연결: ${session.endedStatus}`);
}

메서드

agent.on('call_start', async (call) => {
  call.metadata.customerId = 'CUST_123';

  await call.sendAudio(ulawBytes);    // 오디오 전송 (G.711 μ-law, 8kHz)
  await call.clearAudio();            // 오디오 큐 초기화 (인터럽트 시)
  await call.hangup();                // 통화 종료
  await call.transfer('01012345678'); // 다른 번호로 통화 전환
  await call.wait();                  // 통화 종료까지 대기 (아웃바운드 시 유용)
});

sendAudio() 는 G.711 μ-law (8kHz, mono) 를 받습니다. PCM16 이 아닙니다. 전송 계층이 받은 바이트를 μ-law 로 그대로 해석하므로, PCM16 을 넣으면 예외 없이 잡음으로 재생됩니다. 직접 오디오를 만들어 넣을 때는 먼저 μ-law 로 변환하세요. Pipeline 모드를 쓰면 SDK 가 변환을 대신하므로 이 규격을 다룰 필요가 없습니다.

await call.wait()는 통화가 종료될 때까지 대기합니다. 주로 아웃바운드 단건 발신 시 통화가 끝나기를 기다리는 데 사용합니다. 상대가 받지 않아도(무응답) 발신 취소 시점에 리턴하므로, 성사 여부는 리턴 후 call.endedStatus 로 확인하세요.