Webhook

통화·녹음 이벤트를 받고 HMAC 서명을 검증하는 방법.

통화가 시작·종료될 때 gateway가 지정한 주소로 서명된 JSON을 POST합니다. 폴링 없이 결과를 받는 가장 짧은 길입니다.

Webhook 등록

설정 › 웹훅 한 카드에서 끝납니다 — 아래 넷이 그 카드의 행 순서 그대로입니다.

설정 › 웹훅의 통화 이벤트 전송 카드
  1. 1

    활성화를 켜고 수신 URL을 저장

    URL은 https여야 합니다. 저장은 카드 아래 [저장] 한 번으로 카드 전체가 함께 커밋됩니다.

  2. 2

    받을 이벤트 선택

    위 6종을 개별로 켭니다. 기본은 call.started · call.ended 둘뿐이고 나머지 넷은 꺼져 있습니다 — 기존에 받던 전송이 늘어나지 않게 한 것이라, 새 이벤트는 여기서 켜야 옵니다. 전부 끄면 URL이 있어도 아무것도 발사되지 않습니다.

  3. 3

    서명 secret 발급

    평문은 발급 시 한 번만 표시되고 이후에는 존재 여부만 보입니다. 여기서 발급한 것이 워크스페이스 공용 secret이고, 번호가 자기 secret을 따로 두지 않았다면 그 번호의 전송도 이 값으로 서명됩니다.

  4. 4

    (선택) 번호마다 다르게

    번호 상세 › Webhook의 이 번호만 따로 설정 스위치를 켜면 같은 카드가 그 번호용으로 펼쳐집니다 — 활성화 · URL · 이벤트 · secret 넷을 각각 덮을 수 있습니다. 스위치가 꺼져 있으면 전부 위 공통 설정을 따르고, 켠 뒤에도 비워 둔 항목은 공통값을 그대로 씁니다(예: URL만 바꾸고 secret은 공용 유지).

    이 번호에만 전송을 멈추고 싶으면 스위치를 켜고 활성화를 끄면됩니다 — URL을 비우는 것은 “공통 URL로 보내라”는 뜻이라 전송이 멈추지 않습니다.

워크스페이스 Webhook이 꺼져 있으면 번호에서 활성화를 켜도 전송되지 않습니다 — 위 활성화 토글이 전체 차단 스위치입니다.

이벤트 종류

event언제비고
call.started통화가 ringing에 진입할 때발신을 접수했거나 수신 벨이 울린 시점입니다. 아직 상대가 받기 전입니다.
call.active상대가 받아 active로 전이할 때실제 연결 시점입니다. 발신 성공 여부는 이 이벤트로 판정하세요.
call.endedcompleted · 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입니다.

페이로드

POST→ 여러분의 Webhook URL
{
  "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_atcall.ended는 ended_at, 그 외 통화 이벤트는 started_at, 무음·녹음은 그 사건이 일어난 시각
call.workspace_number우리 쪽 번호 (국내형)
call.remote_number상대 번호 (국내형)
call.end_reason종료 사유 14종 — Calls API › 통화 객체 참고
call.duration_secondsstarted_at ~ ended_at 초 (정수)
call.summary내장 Agent 통화의 자동 요약. 자체 Agent 통화는 null

end_reason · duration_seconds · summary 세 필드는 통화가 끝나기 전(call.started · call.active)에는 항상 null입니다.

무음 · 녹음 이벤트 페이로드

call 블록은 위와 같고 data가 하나 더 붙습니다. 담기는 키는 이벤트마다 다릅니다. 재생 URL은 담기지 않습니다 — 서명 링크에는 만료가 있어 이벤트에 실으면 받는 쪽이 갱신을 떠안게 됩니다. 필요할 때 녹음 링크 API로 발급받으세요.

POST→ 여러분의 Webhook URL
{
  "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-Signaturehex 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.idGET /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로 응답하고, 처리는 큐에 넘기세요.