서버 연동

웹훅 설정

서명 검증과 재시도까지 웹훅 수신 준비를 한 번에 끝내요.

결제 완료·취소·구독 변경 등 이벤트 발생 시 서버로 자동 알림을 보내는 기능이에요.

이럴 때 필요해요​: 결제/주문/구독 상태 변경을 서버에서 실시간 수신할 때
내가 할 일​: 서버 (백엔드) — 웹훅 수신 엔드포인트 구현

설정 순서

  • ① 웹훅 URL 등록 -> ② 이벤트 처리 코드 구현

웹훅 URL 등록

부트페이 관리자 > 개발자 설정 > 웹훅 설정에서 웹훅을 수신할 HTTPS 엔드포인트를 등록해요.

::: warning 웹훅 URL은 반드시 HTTPS​​여야 해요. HTTP URL은 보안상 지원하지 않아요.

이벤트 처리 코드 구현

모든 이벤트가 등록한 URL로 수신돼요. 서버에서 이벤트 타입별로 분기 처리해요. :::

결제 웹훅

이벤트 목록

status 설명 가맹점이 할 일
1 결제 완료 주문 상태 업데이트​, 서비스 활성화
20 결제 취소 재고 복구​, 서비스 비활성화
2 가상계좌 발급 (입금 대기) 입금 안내 표시
가상계좌 흐름

가상계좌는 웹훅이 2번 온다: 발급 시(status: 2) → 입금 완료 시(status: 1).

페이로드 구조

{
  "receipt_id": "6721abc123def456...",
  "order_id": "your_order_id",
  "price": 1000,
  "tax_free": 0,
  "order_name": "테스트 상품",
  "pg": "kcp",
  "method": "card",
  "method_symbol": "card",
  "status": 1,
  "status_locale": "결제완료",
  "purchased_at": "2024-01-01T12:00:00+09:00",
  "card_data": {
    "card_approve_no": "12345678",
    "card_no": "1234-****-****-5678",
    "card_company": "신한카드"
  }
}json
필드 타입 설명
receipt_id String Bootpay 영수증 ID (검증·취소용)
order_id String 가맹점 주문번호
price Number 결제 금액
tax_free Number 비과세 금액
status Number 1: 완료, 2: 대기, 20: 취소
method String 결제수단 (card, bank, phone 등)
purchased_at String 결제 완료 시각

수신 코드

순서 발신 수신 내용
1 BOOTPAY 웹훅 서버 웹훅 전송 / receipt_id / status / price
2 웹훅 서버 결제 조회 API receipt_id로 검증 조회
3 결제 조회 API 웹훅 서버 실제 status / price 반환
3 웹훅 서버 웹훅 서버 위변조 여부 확인
4 웹훅 서버 주문 DB 상태별 주문 업데이트
5 웹훅 서버 BOOTPAY HTTP 200 응답
app.post('/webhook/bootpay', async (req, res) => {
    const { receipt_id, status, price, order_id } = req.body

    // 1. 결제 조회 API로 검증 (위변조 방지)
    const receipt = await Bootpay.getReceipt(receipt_id)

    if (receipt.status !== status || receipt.price !== price) {
        return res.status(200).json({ status: 200 }) // 200은 반환하되 처리 안 함
    }

    // 2. 상태별 처리
    switch (status) {
        case 1: // 결제 완료
            await db.orders.update({
                bootpay_receipt_id: receipt_id,
                amount: price,
                status: 'paid',
                paid_at: new Date()
            }, { where: { order_id } })
            break
        case 20: // 결제 취소
            await db.orders.update(
                { status: 'refunded' },
                { where: { bootpay_receipt_id: receipt_id } }
            )
            break
    }

    res.status(200).json({ status: 200 })
})javascript
반드시 결제 조회 API로 검증해요

웹훅 데이터는 위변조될 수 있어요. receipt_id결제 조회 API를 호출해 금액과 상태를 반드시 검증한 뒤 처리해요.

재시도 정책

웹훅 수신 실패 시 지수 백오프로 자동 재시도해요.

재시도 간격
1회 1분 후
2회 5분 후
3회 30분 후
4회 2시간 후
5회 24시간 후

커머스 웹훅

주문 이벤트

이벤트 설명 가맹점이 할 일
order.done 주문 결제 완료 재고 차감​, 서비스 활성화
order.cancelled 주문 전체 취소 재고 복구​, 서비스 비활성화
order.partial_cancelled 부분 취소 해당 상품만 재고 복구
order.payment_pending 지연 결제 대기 (무통장 등) 입금 안내 표시
order.payment_error 결제 실패 에러 로그 기록

구독 이벤트

이벤트 설명 가맹점이 할 일
subscription.requested_done 구독 신청 접수 관리자에게 승인 요청 알림
subscription.approved 구독 승인 서비스 활성화 (service_active = true)
subscription.renewed 회차 결제 성공 (갱신) 서비스 기간 연장, 다음 결제일 업데이트
subscription.modified 구독 내용 변경 변경 내용 반영
subscription.paused 구독 일시정지 서비스 일시 제한
subscription.resumed 구독 재개 서비스 재활성화
subscription.terminated 구독 해지 (고객) 서비스 비활성화 (service_active = false)
subscription.admin_terminated 구독 해지 (관리자) 서비스 비활성화 + 고객 안내
subscription.expired 약정 만료 서비스 비활성화
subscription.trial_started 체험 시작 서비스 활성화 (제한적)

페이로드 구조

{
  "project_key": "your-project-uuid",
  "idempotency_key": "unique-key-for-deduplication",
  "webhook_type": "order.done",
  "payload": {
    "order_id": "68707c59b0eacea5cd974efd",
    "order_number": "ORDER_12345",
    "price": 29900,
    "status": 2,
    "purchased_at": "2024-01-01T12:00:00+09:00",
    "products": [...],
    "user": { "user_id": "...", "username": "..." }
  }
}json
필드 타입 설명
project_key String 프로젝트 식별자
idempotency_key String 중복 처리 방지를 위한 고유 키
webhook_type String 이벤트 타입 (order.done, subscription.approved 등)
payload Object 이벤트별 데이터 (주문/구독 상세)

수신 코드

app.post('/webhook/commerce', async (req, res) => {
    const { idempotency_key, webhook_type, payload } = req.body

    // 1. 중복 체크
    const exists = await db.webhookLogs.findOne({ where: { idempotency_key } })
    if (exists) {
        return res.status(200).json({ success: true })
    }

    // 2. 로그 저장
    await db.webhookLogs.create({ idempotency_key, event: webhook_type, status: 'received' })

    // 3. 즉시 200 반환
    res.status(200).json({ success: true })

    // 4. 비동기 처리
    switch (webhook_type) {
        case 'order.done':
            await db.orders.update(
                { status: 'paid', bootpay_order_id: payload.order_id, paid_at: new Date() },
                { where: { bootpay_order_number: payload.order_number } }
            )
            break
        case 'order.cancelled':
            await db.orders.update(
                { status: 'refunded' },
                { where: { bootpay_order_id: payload.order_id } }
            )
            break
        case 'subscription.approved':
            await db.subscriptions.update(
                { service_active: true, status: 'active' },
                { where: { bootpay_subscription_id: payload.order_subscription_id } }
            )
            break
        case 'subscription.terminated':
        case 'subscription.admin_terminated':
            await db.subscriptions.update(
                { service_active: false, status: 'terminated' },
                { where: { bootpay_subscription_id: payload.order_subscription_id } }
            )
            break
    }
})javascript

재시도 정책

설정
응답 제한 시간 30초
최대 재시도 25회 (기본 3회)
재시도 조건 HTTP 200 이외 응답 또는 타임아웃

공통 주의사항

  • HTTPS 필수​: 웹훅 URL은 반드시 HTTPS여야 해요
  • 빠른 응답​: 200 응답을 먼저 반환하고, 비즈니스 로직은 비동기로 처리해요
  • 멱등성​: 동일 이벤트가 여러 번 올 수 있으므로 중복 처리를 방지해요

웹훅 수신 서버에서 비즈니스 로직 처리 중 에러가 발생해도, 반드시 HTTP 200을 먼저 반환해야 해요. 그렇지 않으면 불필요한 재시도가 발생해요.

에러 코드

공통 에러

인증·권한 관련 에러는 에러 코드표를 참고해요.

코드 메시지 대처 방법
WEBHOOK_LOG_NOT_FOUND 웹훅 로그 정보가 없어요. webhook_log_id를 확인해요
WEBHOOK_LOG_URL_BLANK 웹훅 로그 URL이 설정되지 않았어요. 웹훅 URL을 설정해요
WEBHOOK_URL_BLANK 웹훅 URL을 입력해요 url 파라미터를 입력해요
WEBHOOK_HEADER_CONTENT_TYPE_INVALID 웹훅 Content Type 선택이 잘못됐어요. 다시 확인해요. application/json 또는 application/x-www-form-urlencoded를 사용해요
WEBHOOK_RETRY_COUNT_INVALID 웹훅 재시도는 최소 1회부터 최대 25회까지 설정할 수 있어요 1~25 사이 값을 설정해요

로컬에서 테스트

# ngrok으로 로컬 서버를 외부에 노출
npx ngrok http 3000
# → https://abc123.ngrok.io 주소를 웹훅 URL로 등록bash
더 자세한 웹훅 처리 가이드

이벤트별 코드 예시, 멱등성 보장 패턴, 디버깅 체크리스트는 웹훅 처리 가이드를 참고해요.