이 문서는 빌링키를 발급하고 원하는 시점의 결제를 예약하는 빠른 매뉴얼이에요. 예약 조회·취소·운영 주의사항과 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 }),
})
}
}javascriptlet payload = Payload()
payload.applicationId = BootpayConfig.applicationId
payload.pg = "나이스페이"
payload.method = "카드자동"
payload.subscriptionId = String(Date().timeIntervalSince1970)
payload.price = 0
payload.orderName = "카드 등록"
payload.extra = BootExtra()
payload.extra?.subscribeTestPayment = true
Bootpay.requestSubscription(viewController: self, payload: payload)
.onDone { data in
// data의 receipt_id를 백엔드 /api/billing-key/save로 전송
self.saveBillingKey(receiptId: data["receipt_id"] as? String ?? "")
}
.onError { _ in }
.onCancel { _ in }swiftval extra = BootExtra().setSubscribeTestPayment(true)
val payload = Payload()
.setApplicationId(BootpayConfig.applicationId)
.setOrderName("카드 등록")
.setPg("나이스페이")
.setMethod("카드자동")
.setSubscriptionId("sub_${System.currentTimeMillis()}")
.setPrice(0.0)
.setExtra(extra)
Bootpay.init(this).setPayload(payload)
.setEventListener(object : BootpayEventListener {
override fun onDone(data: String) {
// data의 receipt_id를 백엔드로 전송
saveBillingKey(data)
}
override fun onError(data: String) {}
override fun onCancel(data: String) {}
override fun onClose() { Bootpay.removePaymentWindow() }
override fun onIssued(data: String) {}
override fun onConfirm(data: String) = true
}).requestSubscription()kotlinfinal payload = Payload()
..webApplicationId = BootpayEnvConfig.webApplicationId
..androidApplicationId = BootpayEnvConfig.androidApplicationId
..iosApplicationId = BootpayEnvConfig.iosApplicationId
..pg = '나이스페이'
..method = '카드자동'
..subscriptionId = DateTime.now().millisecondsSinceEpoch.toString()
..price = 0
..orderName = '카드 등록'
..extra = (Extra()..subscribeTestPayment = 1);
Bootpay().requestSubscription(
context: context,
payload: payload,
onDone: (data) {
// data의 receipt_id를 백엔드 /api/billing-key/save로 전송
saveBillingKey(data);
},
onError: (data) => debugPrint('error: $data'),
onCancel: (data) => debugPrint('cancel: $data'),
onClose: () {},
);dartconst payload = {
pg: '나이스페이',
method: '카드자동',
order_name: '카드 등록',
subscription_id: `sub_${Date.now()}`,
price: 0,
}
const user = { username: '홍길동', phone: '01012345678' }
const extra = { subscribe_test_payment: true }
bootpay.current?.requestSubscription(payload, [], user, extra)
function onDone(data) {
// receipt_id를 백엔드 /api/billing-key/save로 전송
saveBillingKey(data.receipt_id)
}javascript상세 문서: 빌링키 발급
빌링키 발급 B — REST API로 직접 발급 (백엔드, 선택)
일부 PG는 백엔드 REST API로 빌링키를 직접 발급할 수 있어요. 이 방식은 가맹점 화면에서 카드 정보를 입력받아 서버로 전달하고, 서버가 Bootpay API에 빌링키 발급을 요청해요.
나이스페이먼츠, 페이앱, 웰컴페이먼츠, 토스페이먼츠, 키움페이
카드번호·유효기간·생년월일·비밀번호 앞 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# Bootpay SDK 호출 (Flask·Django 등 핸들러 안에서)
info = bootpay.request_subscribe_billing_key(
pg='nicepay',
subscription_id=subscription_id,
order_name='카드 등록',
card_no=card_no,
card_pw=card_pw,
card_identity_no=card_identity_no,
card_expire_year=card_expire_year,
card_expire_month=card_expire_month,
)
# info['billing_key']를 DB에 저장python// Bootpay SDK 호출 (Laravel·Slim 등 컨트롤러 안에서)
$info = BootpayApi::requestSubscribeBillingKey([
'pg' => 'nicepay',
'subscription_id' => $subscriptionId,
'order_name' => '카드 등록',
'card_no' => $cardNo,
'card_pw' => $cardPw,
'card_identity_no' => $cardIdentityNo,
'card_expire_year' => $cardExpireYear,
'card_expire_month' => $cardExpireMonth,
]);
// $info['billing_key']를 DB에 저장php상세 문서: 백엔드에서 발급하기
빌링키 저장 (백엔드)
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# Bootpay SDK 호출 (Flask·Django 등 핸들러 안에서)
info = bootpay.lookup_subscribe_billing_key(receipt_id)
# info['billing_key'] / info['card_name'] / info['card_no']를 DB에 저장python// Bootpay SDK 호출 (Laravel·Slim 등 컨트롤러 안에서)
$info = BootpayApi::lookupSubscribeBillingKey($receiptId);
// $info['billing_key'] / $info['card_name'] / $info['card_no']를 DB에 저장php// Bootpay SDK 호출 (Spring 컨트롤러 안에서)
var info = bootpay.lookupBillingKey(receiptId);
// info.get("billing_key") / info.get("card_name") / info.get("card_no")를 DB에 저장java# Bootpay SDK 호출 (Rails 컨트롤러 안에서)
info = bootpay.request(method: :get, uri: "subscribe/billing_key/#{receipt_id}").data
# info['billing_key'] / info['card_name'] / info['card_no']를 DB에 저장ruby// Bootpay SDK 호출 (net/http·Gin 핸들러 안에서)
info, err := api.LookupBillingKey(receiptID)
if err != nil {
log.Fatal(err)
}
// info["billing_key"] / info["card_name"] / info["card_no"]를 DB에 저장go// Bootpay SDK 호출 (ASP.NET Core 컨트롤러 안에서)
var infoResponse = await bootpay.LookupBillingKey(receiptId);
var infoJson = await infoResponse.Content.ReadAsStringAsync();
// infoJson에서 data.billing_key / card_name / card_no를 추출해 DB에 저장csharp상세 문서: 빌링키 조회 — 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# Bootpay SDK 호출 (Flask·Django 핸들러 안에서)
result = bootpay.subscribe_payment_reserve(
billing_key=billing_key,
price=amount,
order_name=description,
order_id=f'reserve_{int(time.time())}',
reserve_execute_at=execute_at,
)
# result['reserve_id']를 reserve_payments 테이블에 저장python// Bootpay SDK 호출 (Laravel·Slim 컨트롤러 안에서)
$result = BootpayApi::subscribePaymentReserve([
'billing_key' => $billingKey,
'price' => $amount,
'order_name' => $description,
'order_id' => 'reserve_' . time(),
'reserve_execute_at' => $executeAt,
]);
// $result['reserve_id']를 reserve_payments 테이블에 저장php// Bootpay SDK 호출 (Spring 컨트롤러 안에서)
SubscribePayload payload = new SubscribePayload();
payload.billingKey = billingKey;
payload.price = amount;
payload.orderName = description;
payload.orderId = "reserve_" + System.currentTimeMillis();
payload.reserveExecuteAt = executeAt;
var result = bootpay.reserveSubscribe(payload);
// result.get("reserve_id")를 reserve_payments 테이블에 저장java# Bootpay SDK 호출 (Rails 컨트롤러 안에서)
result = bootpay.request(
uri: 'subscribe/payment/reserve',
payload: {
billing_key: billing_key,
price: amount,
order_name: description,
order_id: "reserve_#{Time.now.to_i}",
reserve_execute_at: execute_at
}
).data
# result['reserve_id']를 reserve_payments 테이블에 저장ruby// Bootpay SDK 호출 (net/http·Gin 핸들러 안에서)
result, err := api.ReserveSubscribe(bootpay.SubscribePayload{
BillingKey: billingKey,
Price: amount,
OrderName: description,
OrderId: fmt.Sprintf("reserve_%d", time.Now().Unix()),
ReserveExecuteAt: executeAt,
})
if err != nil {
log.Fatal(err)
}
// result.ReserveID를 reserve_payments 테이블에 저장go// Bootpay SDK 호출 (ASP.NET Core 컨트롤러 안에서)
var result = await bootpay.ReserveSubscribe(new SubscribePayload
{
billingKey = billingKey,
price = amount,
orderName = description,
orderId = $"reserve_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}",
reserveExecuteAt = executeAt,
});
// result.ReserveId를 reserve_payments 테이블에 저장csharp상세 문서: 결제 예약 — 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# Bootpay SDK 호출
response = bootpay.cancel_subscribe_reserve(reserve_id)python// Bootpay SDK 호출
$response = BootpayApi::cancelSubscribeReserve($reserveId);php// Bootpay SDK 호출
var response = bootpay.reserveCancelSubscribe(reserveId);java# Bootpay SDK 호출
response = bootpay.request(method: :delete, uri: "subscribe/payment/reserve/#{reserve_id}").dataruby// Bootpay SDK 호출
response, err := api.ReserveCancelSubscribe(reserveID)
if err != nil {
log.Fatal(err)
}go// Bootpay SDK 호출
var response = await bootpay.ReserveCancelSubscribe(reserveId);csharp상세 문서: 예약 취소 — 7개 언어 예제와 에러 코드를 확인해요.
다음 단계
- 예약 조회·취소·운영 주의사항을 자세히 보려면 → 예약결제 상세 문서
- 결제수단 변경·빌링키 삭제 순서를 정리하려면 → 예약결제 운영 주의사항
- 매월 자동으로 반복 결제하려면 → 자동결제
더 읽을거리
- 결제수단 선택 설계 — 카드 등록 UX·동의 문구
- 결제 실패 재시도 정책 — 예약 실패 시 대응
