빠른 매뉴얼

결제위젯

쿠폰·적립금·프로모션으로 금액이 바뀌는 주문서에 위젯을 붙여봐요.

이 문서는 결제위젯을 주문서에 빠르게 붙이기 위한 매뉴얼이에요. 위젯 생성·디자인·운영 옵션은 위젯 생성단건결제위젯 연동에서 더 자세히 다뤄요.

아래 순서대로 진행하면 주문서 화면에서 결제위젯을 렌더링하고 서버에서 결제 결과를 확인하는 기본 흐름을 만들 수 있어요. 관리자는 위젯에 노출할 결제수단과 스타일을 설정하고, 사용자는 주문서에 렌더링된 위젯 안에서 결제수단을 선택해요. 백엔드는 주문 확인과 상태 저장을 담당해요.

데모 영상

결제창 vs 결제위젯

결제창 결제위젯
결제수단 선택 코드에서 pg, method 지정 사용자가 위젯에서 선택
UI PG사 결제창만 결제수단 선택 UI + 약관 동의 포함
결제수단 관리 코드 수정 필요 관리자에서 노출 수단 활성화/비활성화 및 스타일 설정
추천 결제수단이 1~2개로 고정 여러 결제수단을 유연하게 관리

완성 후 모습

위젯은 관리자가 활성화한 결제수단을 주문서 화면에 보여주고, 사용자는 그 안에서 결제수단 선택과 약관 동의를 진행해요. 결제 완료 후 receipt_id를 받은 뒤부터는 백엔드가 결제 정보를 검증하고 주문을 확정해야 해요.

서버 승인을 쓰는 경우

이 빠른 매뉴얼의 기본 예제는 클라이언트 자동 승인 후 done으로 receipt_id를 받는 흐름이에요. 운영에서 extra.separately_confirmed: true를 쓰면 done 대신 confirm이 호출되고, 클라이언트의 onDone은 호출되지 않아요. 이때는 confirm에서 receipt_id를 서버로 보내고 서버가 승인 API를 호출해야 해요.

사전 준비

결제 설정에서 사용할 PG사와 결제수단을 먼저 활성화해요. 그다음 위젯 설정에서 사용할 위젯을 만들고, 생성된 widget_key를 연동 코드에 넣어요.

구현 순서

SDK 설치 (프론트엔드)

플랫폼별 표준 패키지 매니저로 설치해요.

<!-- Bootpay JS SDK (CDN) -->
<script src="https://js.bootpay.co.kr/bootpay-5.3.0.min.js"></script>

<!-- 또는 npm -->
<!-- npm install @bootpay/client-js -->html

위젯 렌더링 (프론트엔드)

사용 중인 SDK 탭에서 widget_key를 어디에 넘기는지 확인해요.

<!-- 위젯이 렌더링될 영역 -->
<div id="widget"></div>
<button id="pay-btn" disabled onclick="requestPayment()">결제하기</button>

<script>
let widgetData = null
const payButton = document.getElementById('pay-btn')

function updateButtonState(data) {
    widgetData = data
    // completed + term_passed 가 모두 true 일 때만 결제 버튼을 활성화한다.
    payButton.disabled = !(data?.completed && data?.term_passed)
}

// JavaScript는 BootpayWidget.render(...)에 위젯 설정과 이벤트 hooks를 함께 넘겨요.
// 이벤트는 더 이상 document.addEventListener가 아니라 hooks 콜백으로 받아요.
BootpayWidget.render('#widget', {
    client_key: 'YOUR_CLIENT_KEY',   // 인증 키. application_id로 대체 가능
    price: 1000,                     // 결제 금액. 쿠폰·적립금 반영 후 최종 금액
    tax_free: 0,                     // 면세 금액 (선택, 기본 0)
    widget_key: 'default-widget',    // 관리자에서 만든 위젯 키
    sandbox: true,                   // true면 테스트(샌드박스) 모드
    use_terms: true,                 // 위젯에서 약관 동의 UI 사용 여부 (선택, 기본 true)
    hooks: {
        ready: () => {
            // 위젯 SDK가 준비된 시점이다.
            console.log('위젯 준비 완료')
        },
        paymentMethodUpdated: (data) => {
            // 결제수단 변경 이벤트. data 안에 pg, method, easy_pay, card_quota 등이 들어온다.
            updateButtonState(data)
        },
        termsConsentUpdated: (data) => {
            // 약관 동의 변경 이벤트. data.term_passed 로 필수 약관 통과 여부를 확인한다.
            updateButtonState(data)
        },
        allTermsAccepted: (data) => {
            // 전체 약관 동의 이벤트. 필요하면 동의 완료 상태를 별도로 처리한다.
        },
        resize: (data) => {
            // 위젯 높이가 바뀌면 컨테이너 높이도 같이 맞춘다.
            document.getElementById('widget').style.height = `${data.height}px`
        }
    }
})
</script>html

상세 문서: 위젯 렌더링 옵션

WidgetData 예시

위젯의 결제수단이나 약관 상태가 바뀌면 아래와 비슷한 데이터가 이벤트로 전달돼요.

{
  "pg": "nicepay",
  "method": "card",
  "method_symbol": "card",
  "easy_pay": null,
  "card_quota": "0",
  "currency": "KRW",
  "completed": true,
  "term_passed": true,
  "select_terms": [
    {
      "pk": "terms-privacy",
      "title": "개인정보 수집 및 이용 동의",
      "agree": true,
      "term_type": 1
    }
  ],
  "extra": {
    "card_quota": 0
  }
}json

결제 요청 (프론트엔드)

사용자가 위젯에서 결제수단 선택과 약관 동의를 완료하면, 각 SDK의 위젯 결제 요청 API를 호출해야 해요. 완료 이벤트에서 받은 receipt_id는 백엔드로 보내 결제 조회를 진행해야 해요.

완료 이벤트 데이터는 상태별로 아래처럼 들어와요. 결제창과 결제위젯의 최종 조회 응답은 같으므로, 서버에서는 결제 조회 응답 예시의 필드를 기준으로 주문을 확정해요.

{
  "event": "done",
  "receipt_id": "6244f60c1fc19202e42e8c4e",
  "order_id": "order_20260428_001",
  "price": 1000,
  "status": 1,
  "method": "card",
  "method_symbol": "card"
}json
async function requestPayment() {
    // price, pg, method는 render()와 위젯에서 선택한 값이 자동으로 합쳐지므로 여기서 넘기지 않아요.
    const response = await BootpayWidget.requestPayment({
        order_name: '테스트 상품',           // 주문명 (필수)
        order_id: 'order_' + Date.now(),    // 가맹점 주문 고유번호 (필수)
        user: {                             // 구매자 정보 (선택)
            id: 'user_1234',
            username: '홍길동',
            phone: '01012345678',
            email: 'test@bootpay.co.kr',
        },
        items: [                            // 상품 목록 (선택)
            { id: 'item_1', name: '테스트 상품', qty: 1, price: 1000 },
        ],
        metadata: { cart_id: 'cart_1234' }, // 가맹점 커스텀 데이터 (선택)
        extra: {
            open_type: 'iframe',            // iframe | popup | redirect
            separately_confirmed: false,    // true면 done 대신 confirm 이벤트가 호출됨
        },
    })

    if (response.event === 'done') {
        // 결제 완료 이벤트. receipt_id를 백엔드로 보내 결제 조회를 진행한다.
        await verifyPayment(response.receipt_id)
    }

    if (response.event === 'issued') {
        // 가상계좌 발급 이벤트. 입금 완료는 웹훅으로 최종 확정한다.
    }

    if (response.event === 'cancel' || response.event === 'error') {
        // 구매자 취소 또는 결제 실패 이벤트. 주문은 미결제 상태로 유지한다.
    }
}

async function verifyPayment(receiptId) {
    const res = await fetch('/api/server/verify', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ receipt_id: receiptId }),
    })
    const { success } = await res.json()
    alert(success ? '결제 완료!' : '결제 조회 실패')
}javascript

위젯에서는 pg, method를 코드에 하드코딩하지 않아요. 관리자가 활성화한 결제수단이 위젯에 표시되고, 사용자가 선택한 결과를 그대로 결제 요청 페이로드에 합쳐요.

상세 문서: 결제위젯 가이드

서버 SDK 설치 (백엔드)

SDK 설치

PG 결제창 연동 → SDK 설치의 서버 SDK 단계를 먼저 완료하고 API 인증까지 마쳐요.

결제 조회 (백엔드)

결제창과 동일한 SDK 호출이에요. 프론트엔드에서 받은 receipt_id는 누락·조작될 수 있으므로, 서버가 Bootpay API로 다시 조회해 주문을 확정해야 해요. 응답 필드는 결제 조회 응답 예시의 JSON과 같아요.

서버에서는 최소한 아래 값을 내부 주문과 비교해요.

확인 기준
status 일반 결제 완료는 1인지 확인해요
price 내부 주문 금액과 일치하는지 확인해요
order_id 같은 결제 건인지 내부 주문 ID와 비교해요
receipt_id 이미 처리한 영수증이면 중복 처리하지 않아요 (멱등)

아래 예제는 statusprice만 비교하는 최소 형태예요. 운영에서는 order_id 일치 확인과 중복 처리 방지까지 함께 적용해요. 자세한 검증·상태 코드 기준은 결제 조회를 참고해요.

app.post('/api/server/verify', async (req, res) => {
    const { receipt_id } = req.body
    const receipt = await Bootpay.receiptPayment(receipt_id)

    if (receipt.status === 1 && receipt.price === 1000) {
        await db.payments.create({
            receipt_id,
            price: receipt.price,
            method: receipt.method_symbol,
            paid_at: receipt.purchased_at,
        })
        res.json({ success: true })
    } else {
        await Bootpay.cancelPayment({
            receipt_id,
            cancel_price: receipt.price,
            cancel_message: '검증 실패',
        })
        res.json({ success: false })
    }
})javascript

상세 문서: 결제 조회

웹훅 수신 (백엔드, 선택)

위젯에서 가상계좌(vbank)를 노출하면 입금은 고객이 나중에 진행하므로, 입금 완료 시점은 프론트엔드 issued가 아니라 웹훅으로 확정해야 해요. 관리자에서 결제를 취소하는 경우에도 웹훅으로 주문 상태를 보정해요.

웹훅 데이터도 그대로 믿지 말고 receipt_id로 다시 조회해 위변조를 확인한 뒤, webhook_type으로 분기해요.

app.post('/webhooks/bootpay', async (req, res) => {
    const { receipt_id, status, price, order_id, webhook_type } = req.body

    // 1. 결제 조회 API로 다시 검증 (위변조 방지)
    const receipt = await Bootpay.receiptPayment(receipt_id)
    if (receipt.status !== status || receipt.price !== price) {
        return res.status(200).json({ success: false }) // 200은 반환하되 처리하지 않음
    }

    // 2. webhook_type으로 분기 (status보다 정확함, SDK 5.x.x 이상)
    switch (webhook_type) {
        case 'PAYMENT_COMPLETED':                // 결제·입금 완료 (가상계좌 입금 포함)
            await markOrderPaid(order_id, receipt)
            break
        case 'PAYMENT_VIRTUAL_ACCOUNT_ISSUED':   // 가상계좌 발급 (입금 대기)
            await markOrderPending(order_id, receipt)
            break
        case 'PAYMENT_CANCELLED':                // 전체 취소
        case 'PAYMENT_PARTIAL_CANCELLED':        // 부분 취소
            await markOrderCancelled(receipt_id, receipt)
            break
    }

    res.status(200).json({ success: true })
})javascript

상세 문서: 웹훅 처리 가이드

다음 단계