빠른 매뉴얼

예약결제

원하는 시점에 결제되도록 PG사에 결제를 예약해요.

이 문서는 빌링키를 발급하고 원하는 시점의 결제를 예약하는 빠른 매뉴얼이에요. 예약 조회·취소·운영 주의사항과 API 세부 옵션은 예약결제 상세 문서에서 확인해요.

예약결제는 빌링키​​로 원하는 시점의 결제를 미리 등록하는 방식이에요. 고객이 결제창에 카유생비​(카드번호, 유효기간, 생년월일, 비밀번호 앞 2자리)를 입력하면 PG/카드사가 카드 정보를 확인하고 빌링키를 발급해요. 가맹점 서버는 카드 정보를 직접 저장할 수 없기 때문에, 실제 카드 정보 대신 결제에 사용할 수 있는 암호화된 식별값을 저장해야 해요. 이 값이 빌링키예요.

빌링키 발급 방식은 두 가지예요. 보통은 프론트엔드에서 PG 결제창을 띄워 고객이 직접 입력하는 방식​​을 써요. 일부 PG에서는 백엔드 REST API로 카드 정보를 전달해 직접 발급하는 방식​​도 지원해요. 어느 방식이든 최종적으로 백엔드가 billing_key를 저장하고, 그 빌링키로 Bootpay API에 결제 예약을 등록해야 해요. 예약을 보관했다가 예약 시각에 결제를 실행하는 주체는 부트페이​​예요. 부트페이는 실행하는 그 순간에만 PG사로 빌링키 결제를 요청하고, 결과를 웹훅으로 알려줘요.

데모 영상

연동 흐름

1빌링키 발급

2예약 등록과 실행

빌링키 발급과 예약 등록은 별개의 단계예요. 먼저 둘 중 한 방식으로 빌링키를 발급·저장하고, 이후 서버에서 저장된 billing_key로 예약 결제 API를 호출해야 해요.

구현 순서

빌링키 발급 A — PG 결제창에서 발급 (프론트엔드)

권장 방식이에요. 프론트엔드에서 Bootpay SDK로 PG 결제창을 띄우고, 고객이 결제창에 카유생비를 입력하면 빌링키 발급이 진행돼요.

async function registerCard() {
    const response = await Bootpay.requestSubscription({
        client_key: '[ Client Key ]',
        pg: '나이스페이',
        method: '카드자동',
        subscription_id: 'sub_' + Date.now(),  // 고유 ID
        price: 0,  // 0원 = 빌링키만 발급 (결제 없음)
        order_name: '카드 등록',
        user: { username: '홍길동', phone: '01012345678' },
        extra: { subscribe_test_payment: true },  // 100원 테스트 결제 후 자동 취소
    })

    if (response.event === 'done') {
        await fetch('/api/billing-key/save', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ receipt_id: response.receipt_id }),
        })
    }
}javascript

상세 문서: 빌링키 발급

빌링키 발급 B — REST API로 직접 발급 (백엔드, 선택)

일부 PG는 백엔드 REST API로 빌링키를 직접 발급할 수 있어요. 이 방식은 가맹점 화면에서 카드 정보를 입력받아 서버로 전달하고, 서버가 Bootpay API에 빌링키 발급을 요청해요.

REST API 직접 발급 지원 PG사

나이스페이먼츠, 페이앱, 웰컴페이먼츠, 토스페이먼츠, 키움페이

카드번호·유효기간·생년월일·비밀번호 앞 2자리를 가맹점 시스템이 직접 취급하므로, 지원 PG 여부와 보안 책임을 먼저 확인해야 해요. 가능하면 A 방식처럼 PG 결제창에서 입력받는 흐름을 우선 검토해요.

app.post('/api/billing-key/issue', auth, async (req, res) => {
    const { card_no, card_pw, card_identity_no, card_expire_year, card_expire_month } = req.body

    const info = await Bootpay.requestSubscribeBillingKey({
        pg: '나이스페이',
        subscription_id: `sub_${req.user.id}_${Date.now()}`,
        order_name: '카드 등록',
        card_no,
        card_pw,              // 비밀번호 앞 2자리
        card_identity_no,     // 생년월일 6자리 또는 사업자등록번호 10자리
        card_expire_year,     // YY
        card_expire_month,    // MM
    })

    await db.billingKeys.create({
        user_id: req.user.id,
        billing_key: info.billing_key,
        card_name: info.card_name,
        card_no_last4: info.card_no?.slice(-4),
        status: 'active'
    })

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

상세 문서: 백엔드에서 발급하기

빌링키 저장 (백엔드)

billing_keys 테이블 스키마는 자동결제와 동일해요 → 데이터 저장 설계.

아래 코드는 A 방식처럼 프론트엔드 결제창에서 받은 receipt_id로 빌링키를 조회해 저장하는 흐름이에요. B 방식은 발급 API 응답에 billing_key가 들어오므로 조회 단계 없이 바로 저장해야 해요.

Node.js는 저장 핸들러까지 포함하고, 다른 언어는 빌링키 조회 SDK 호출부만 보여줘요. 토큰 발급·DB 저장·라우팅은 각 프레임워크 컨벤션에 맞게 연결해요.

app.post('/api/billing-key/save', auth, async (req, res) => {
    const { receipt_id } = req.body

    // Bootpay에서 빌링키 조회
    const info = await Bootpay.lookupSubscribeBillingKey(receipt_id)

    // DB에 빌링키 저장
    await db.billingKeys.create({
        user_id: req.user.id,
        billing_key: info.billing_key,
        card_name: info.card_name,       // "신한카드"
        card_no_last4: info.card_no.slice(-4),
        status: 'active'
    })

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

상세 문서: 빌링키 조회 — 7개 언어 SDK 초기화와 응답 스키마를 확인해요.

예약 결제 등록 (백엔드)

등록 전에 확인할 것
  • feedback_url(또는 관리자 통합 웹훅 URL)이 없으면 예약 등록이 SR_WEBHOOK_URL_BLANK로 거절돼요.
  • reserve_execute_at은 현재로부터 2개월 이내​​여야 하고, 빌링키 만료일보다 뒤일 수 없어요.
  • order_id는 같은 빌링키 안에서 재사용할 수 없어요. 취소·완료된 예약의 값도 마찬가지예요.
app.post('/api/reserve-payment', auth, async (req, res) => {
    const { amount, execute_at, description } = req.body

    // 사용자의 빌링키 조회
    const billingKey = await db.billingKeys.findOne({
        where: { user_id: req.user.id, status: 'active' }
    })
    if (!billingKey) {
        return res.status(400).json({ error: '등록된 카드가 없습니다' })
    }

    // Bootpay API 호출 → 부트페이가 예약을 보관했다가 예약 시각에 실행
    const result = await Bootpay.subscribePaymentReserve({
        billing_key: billingKey.billing_key,
        price: amount,
        order_name: description,
        order_id: 'reserve_' + Date.now(),
        reserve_execute_at: execute_at,  // '2025-03-01T09:00:00 +0900', 2개월 이내
        feedback_url: 'https://example.com/webhooks/bootpay'
    })

    // 예약 정보 저장
    await db.reservePayments.create({
        user_id: req.user.id,
        reserve_id: result.reserve_id,
        amount,
        execute_at,
        description,
        status: 'reserved'
    })

    res.json({
        success: true,
        reserve_id: result.reserve_id,
        execute_at: result.reserve_execute_at
    })
})javascript

상세 문서: 결제 예약 — 7개 언어 예제와 응답 스키마를 확인해요.

웹훅 수신 — 예약 실행 결과 (백엔드)

예약 시간이 되면 부트페이가 저장된 빌링키로 PG사에 결제를 요청하고, 그 결과를 가맹점 서버로 웹훅으로 보내줘요.

웹훅 본문은 event·data로 감싸여 오지 않고 평면 객체​​로 와요. 성공과 실패는 서로 다른 형태로 오기 때문에, error_code가 있는지로 먼저 갈라야 해요.

app.post('/webhooks/bootpay', async (req, res) => {
    res.status(200).json({ success: true })

    // 실패: { error_code, pg_error_code, message, payload: { order_id, reserve_id, metadata } }
    if (req.body.error_code) {
        const { reserve_id } = req.body.payload || {}
        await db.reservePayments.update(
            { status: 'failed', fail_reason: req.body.message },
            { where: { reserve_id } }
        )
        // 고객에게 결제 실패 알림 — 부트페이는 자동으로 재시도하지 않아요
        return
    }

    // 성공: { receipt_id, reserve_id, order_id, price, status, ... }
    const { receipt_id, reserve_id, status } = req.body

    if (reserve_id && status === 1) {
        // receipt_id로 결제를 다시 조회해 금액을 검증한 뒤 확정해요
        await db.reservePayments.update(
            {
                status: 'done',
                receipt_id,
                paid_at: new Date()
            },
            { where: { reserve_id } }
        )
    }
})javascript

성공 본문의 status는 결제 상태값으로 1이 결제 완료예요. 예약 자체의 상태(0 대기 / 1 완료 / -1 실패 / 3 취소)는 예약 조회로 확인하는 값이라 서로 다른 체계예요.

실패는 웹훅만 믿지 말고 조회로 보완해요

실행에 실패하면 위처럼 error_code가 담긴 웹훅이 오지만, 이 본문에는 receipt_id가 없고 매칭 키가 payload.reserve_id에 들어 있어요. 관리자에서 오류 웹훅​​을 켰는지 여부에 따라 본문을 만드는 주체와 세부 필드도 달라져요.

그리고 실패한 예약은 자동으로 재시도되지 않아요. 상태가 -1로 고정되므로, 실행 예정 시각이 지난 예약을 예약 조회로 확인해 status-1인 건을 찾아내는 폴링을 함께 두는 편이 안전해요. 재청구가 필요하면 새 order_id로 예약을 다시 등록하거나 빌링키 결제를 직접 호출해야 해요.

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

예약 취소 (백엔드, 선택)

예약 취소는 부트페이에 예약된 실행 작업을 삭제하는 것​​이에요. 아직 실행되지 않은 예약에만 쓸 수 있고, 이미 결제가 실행된 뒤에는 예약 취소가 아니라 receipt_id 기준 결제 취소를 호출해야 해요.

app.post('/api/reserve-payment/:id/cancel', auth, async (req, res) => {
    const reserve = await db.reservePayments.findByPk(req.params.id)
    if (!reserve || reserve.user_id !== req.user.id) {
        return res.status(404).json({ error: '예약을 찾을 수 없음' })
    }
    if (reserve.status !== 'reserved') {
        return res.status(400).json({ error: '취소할 수 없는 상태' })
    }

    await Bootpay.cancelSubscribeReserve(reserve.reserve_id)

    await db.reservePayments.update(
        { status: 'cancelled' },
        { where: { id: reserve.id } }
    )

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

상세 문서: 예약 취소 — 7개 언어 예제와 에러 코드를 확인해요.

다음 단계

더 읽을거리