빠른 매뉴얼

예약결제

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

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

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

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

데모 영상

연동 흐름

1빌링키 발급

2예약 등록과 실행

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

구현 순서

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

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

async function registerCard() {
    const response = await Bootpay.requestSubscription({
        application_id: 'YOUR_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 초기화와 응답 스키마를 확인해요.

예약 결제 등록 (백엔드)

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 호출 → Bootpay가 PG사에 예약 등록
    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'
    })

    // 예약 정보 저장
    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사가 자체적으로 결제를 실행하고, Bootpay가 그 결과를 받아 가맹점 서버로 웹훅을 보내줘요.

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

    const { event, data } = req.body

    if (event === 'payment.done') {
        // 예약 결제 실행 완료
        await db.reservePayments.update(
            {
                status: 'done',
                receipt_id: data.receipt_id,
                paid_at: new Date()
            },
            { where: { reserve_id: data.reserve_id } }
        )
    }

    if (event === 'payment.failed') {
        // 결제 실패 (잔액 부족 등)
        await db.reservePayments.update(
            { status: 'failed' },
            { where: { reserve_id: data.reserve_id } }
        )
        // 고객에게 결제 실패 알림
    }
})javascript

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

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

예약 시간 전에 취소할 수 있어요. 취소 요청도 Bootpay가 PG사에 전달해 예약을 제거해요.

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개 언어 예제와 에러 코드를 확인해요.

다음 단계

더 읽을거리