Webhook
통화·녹음 이벤트를 받고 HMAC 서명을 검증하는 방법.
통화가 시작·종료될 때 gateway가 지정한 주소로 서명된 JSON을 POST합니다. 폴링 없이 결과를 받는 가장 짧은 길입니다.
Webhook 등록
설정 › 웹훅 한 카드에서 끝납니다 — 아래 넷이 그 카드의 행 순서 그대로입니다.


- 1
활성화를 켜고 수신 URL을 저장
URL은
https여야 합니다. 저장은 카드 아래 [저장] 한 번으로 카드 전체가 함께 커밋됩니다. - 2
받을 이벤트 선택
위 6종을 개별로 켭니다. 기본은
call.started·call.ended둘뿐이고 나머지 넷은 꺼져 있습니다 — 기존에 받던 전송이 늘어나지 않게 한 것이라, 새 이벤트는 여기서 켜야 옵니다. 전부 끄면 URL이 있어도 아무것도 발사되지 않습니다. - 3
서명 secret 발급
평문은 발급 시 한 번만 표시되고 이후에는 존재 여부만 보입니다. 여기서 발급한 것이 워크스페이스 공용 secret이고, 번호가 자기 secret을 따로 두지 않았다면 그 번호의 전송도 이 값으로 서명됩니다.
- 4
(선택) 번호마다 다르게
번호 상세 › Webhook의 이 번호만 따로 설정 스위치를 켜면 같은 카드가 그 번호용으로 펼쳐집니다 — 활성화 · URL · 이벤트 · secret 넷을 각각 덮을 수 있습니다. 스위치가 꺼져 있으면 전부 위 공통 설정을 따르고, 켠 뒤에도 비워 둔 항목은 공통값을 그대로 씁니다(예: URL만 바꾸고 secret은 공용 유지).
이 번호에만 전송을 멈추고 싶으면 스위치를 켜고 활성화를 끄면됩니다 — URL을 비우는 것은 “공통 URL로 보내라”는 뜻이라 전송이 멈추지 않습니다.
워크스페이스 Webhook이 꺼져 있으면 번호에서 활성화를 켜도 전송되지 않습니다 — 위 활성화 토글이 전체 차단 스위치입니다.
이벤트 종류
| event | 언제 | 비고 |
|---|---|---|
call.started | 통화가 ringing에 진입할 때 | 발신을 접수했거나 수신 벨이 울린 시점입니다. 아직 상대가 받기 전입니다. |
call.active | 상대가 받아 active로 전이할 때 | 실제 연결 시점입니다. 발신 성공 여부는 이 이벤트로 판정하세요. |
call.ended | completed · failed · no_answer에 도달할 때 | 종료 사유·통화 길이·요약이 함께 옵니다. |
call.silence_detected | 상대가 응답하지 않아 무음 정책이 동작할 때 | 통화 도중에 옵니다. 상담원 전환·재시도 큐 적재 등에 쓸 수 있습니다. |
recording.completed | 녹음 파일 저장이 끝났을 때 | 녹음 처리는 통화보다 늦게 끝나므로 call.ended 뒤에 도착합니다. |
recording.failed | 녹음이 남지 않았을 때 | 통화 자체는 정상일 수 있습니다. 사유는 data.reason에 옵니다. |
신규 이벤트는 기본으로 꺼져 있습니다 — 설정 › 웹훅에서 켜야 발사됩니다. 기존에 받던 call.started · call.ended는 그대로입니다.
call.silence_detected · recording.* 세 이벤트는 페이로드에 data 필드가 하나 더 있습니다. 담기는 키는 이벤트마다 다릅니다 — 무음은 action, 녹음 완료는 object_key · size_bytes · duration_sec, 녹음 실패는 reason입니다.
페이로드
{
"event": "call.ended",
"event_id": "3f0b8a1e-…",
"occurred_at": "2026-08-11T04:21:07.812000+00:00",
"call": {
"id": "9f1c…",
"direction": "outbound",
"workspace_number": "07079193558",
"remote_number": "01012345678",
"number_id": "6b2e…",
"status": "completed",
"started_at": "2026-08-11T04:20:24.106000+00:00",
"ended_at": "2026-08-11T04:21:07.812000+00:00",
"end_reason": "agent_hangup",
"duration_seconds": 43,
"summary": "8월 3일 오후 2시 예약을 안내하고 상대가 확인함."
}
}| 필드 | 설명 |
|---|---|
event | 이벤트 이름 (위 표의 6종 중 하나) |
event_id | 이 이벤트의 UUID — 재시도해도 같은 값(멱등 키) |
occurred_at | call.ended는 ended_at, 그 외 통화 이벤트는 started_at, 무음·녹음은 그 사건이 일어난 시각 |
call.workspace_number | 우리 쪽 번호 (국내형) |
call.remote_number | 상대 번호 (국내형) |
call.end_reason | 종료 사유 14종 — Calls API › 통화 객체 참고 |
call.duration_seconds | started_at ~ ended_at 초 (정수) |
call.summary | 내장 Agent 통화의 자동 요약. 자체 Agent 통화는 null |
end_reason · duration_seconds · summary 세 필드는 통화가 끝나기 전(call.started · call.active)에는 항상 null입니다.
무음 · 녹음 이벤트 페이로드
call 블록은 위와 같고 data가 하나 더 붙습니다. 담기는 키는 이벤트마다 다릅니다. 재생 URL은 담기지 않습니다 — 서명 링크에는 만료가 있어 이벤트에 실으면 받는 쪽이 갱신을 떠안게 됩니다. 필요할 때 녹음 링크 API로 발급받으세요.
{
"event": "recording.completed",
"event_id": "b7d2…",
"occurred_at": "2026-08-11T04:21:39.204000+00:00",
"call": {
"id": "9f1c…",
"direction": "outbound",
"workspace_number": "07079193558",
"remote_number": "01012345678",
"number_id": "6b2e…",
"status": "completed",
"started_at": "2026-08-11T04:20:24.106000+00:00",
"ended_at": "2026-08-11T04:21:07.812000+00:00",
"end_reason": "agent_hangup",
"duration_seconds": 43,
"summary": "8월 3일 오후 2시 예약을 안내하고 상대가 확인함."
},
"data": {
"object_key": "6b2e…/9f1c….ogg",
"size_bytes": 25645,
"duration_sec": 41
}
}서명 검증
secret을 저장해 두면 요청에 서명 헤더 두 개가 함께 옵니다. 서명은 HMAC-SHA256(secret, `${timestamp}.${원문 바디}`)의 hex 문자열입니다.
| 헤더 | 값 |
|---|---|
X-Gateway-Event | 이벤트 이름 (항상 전송) |
X-Gateway-Timestamp | 유닉스 초 (secret이 있을 때만) |
X-Gateway-Signature | hex HMAC-SHA256 (secret이 있을 때만) |
# 서명은 "{timestamp}.{원문 바디}" 를 secret으로 HMAC-SHA256 한 hex 값입니다.
TS="$(printf '%s' "$X_GATEWAY_TIMESTAMP")"
printf '%s.%s' "$TS" "$RAW_BODY" \
| openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex반드시 수신한 원문 바디로 검증하세요. JSON을 파싱했다가 다시 직렬화하면 공백·유니코드 이스케이프가 달라져 서명이 어긋납니다.
종료 후 정본 조회
call.ended는 종료를 알리는 트리거로 쓰고, 과금처럼 정확한 실제 연결시간이 필요한 처리는 call.id로GET /api/v1/calls/{call_id}을 다시 조회하세요. 응답의 duration_sec은 워커가 answer→disconnect 구간을 monotonic clock으로 측정한 값입니다. 웹훅의 duration_seconds은 started_at→ended_at wallclock이라 ringing이 포함됩니다.
전달 규칙
- 타임아웃 3초, 최대 2회 시도(최초 + 재시도 1회).
- 재시도는 타임아웃 · 네트워크 오류 · 5xx에만 걸립니다. 4xx는 기록만 하고 버립니다.
- 전달은 통화 처리를 막지 않습니다 — 여러분의 서버가 죽어도 통화는 정상 진행됩니다. 바꿔 말해 유실될 수 있습니다. 정합성이 중요하면 종료 이벤트를 상태 조회로 한 번 더 확인하세요.
event_id를 중복 제거 키로 그대로 쓰세요. 재시도는 처음 만든 본문을 그대로 다시 보내므로event_id가 같습니다 — 중복이 생기는 유일한 경로에서 값이 안 변한다는 뜻입니다. (타임아웃으로 재시도했는데 사실은 첫 요청이 처리됐던 경우가 여기 해당합니다.)- 이벤트 하나당 발사도 한 번입니다 — gateway를 여러 대 굴려도 통화가 시작·종료된 그 인스턴스에서만 나갑니다.
- 가능한 한 빨리 2xx로 응답하고, 처리는 큐에 넘기세요.