이 문서는 고객의 결제수단을 등록하고 빌링키를 발급하는 방법을 설명해요.
고객이 결제창에 카드 정보를 등록하면 Bootpay/PG사가 빌링키를 발급해요. 가맹점 서버는 카드 정보를 저장하면 안 되고, 발급 결과를 조회해 billing_key와 표시용 카드 정보만 저장해야 해요.
핵심 요약
- 권장 방식은 프론트엔드에서 Bootpay 결제창을 열어 빌링키를 발급받는 방식이에요.
- 프론트엔드는 카드 정보를 직접 저장하면 안 되고, 서버는 발급 결과를 조회해
billing_key만 저장해야 해요. - 백엔드 직접 발급은 일부 PG에서만 지원하며 카드 정보 보안 책임이 커져요.
- 발급된 빌링키는 빌링키 결제 요청 또는 예약결제에 사용할 수 있어요.
발급 흐름 한눈에
프론트엔드에서 발급하기
PG 결제창을 통해 안전하게 발급하는 방식이에요. 카드 정보가 PG사에서 직접 처리되므로 보안에 유리해요.
KCP, 다날, 이니시스, 나이스페이먼츠, 토스페이먼츠, 웰컴페이먼츠, 키움페이, 페이레터, 페이앱, 라이트페이
발급 요청
Bootpay.requestSubscription 함수를 사용하여 빌링키 발급을 위한 결제창을 띄워요.
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
pg |
String | 필수 | PG사 코드 (미입력 시 SUBSCRIBE_NEED_PG_METHOD 2300) |
method |
String | 필수 | 카드자동 값을 지정 |
subscription_id |
String | 필수 | 가맹점에서 관리하는 고유 주문번호 |
order_name |
String | 필수 | 주문명 (미입력 시 RC_NAME_BLANK 2003) |
price |
Integer | 선택 | 0 또는 100원 이상. 0보다 큰 값이면 빌링키 발급 즉시 결제, 0이면 빌링키만 발급 |
extra.subscribe_test_payment |
Boolean | 선택 | true 지정 시 PG 최소금액(기본 100원)으로 인증결제를 진행하고, 약 10초 뒤 자동으로 취소해요 |
import { Bootpay } from '@bootpay/client-js'
const response = await Bootpay.requestSubscription({
client_key: '[ Client Key ]',
pg: 'nicepay',
method: '카드자동',
order_name: '정기결제 등록',
subscription_id: 'sub_' + Date.now(),
price: 0,
user: {
username: '홍길동',
phone: '01012345678'
},
extra: {
subscribe_test_payment: true
}
})
console.log(response)javascriptval payload = Payload().apply {
clientKey = "[ Client Key ]"
pg = "nicepay"
method = "card_rebill"
orderName = "정기결제 등록"
subscriptionId = "SUB-" + System.currentTimeMillis()
price = 0.0
user = User().apply {
id = "user_123"
username = "홍길동"
phone = "01012345678"
}
extra = Extra().apply {
subscribeTestPayment = true
}
}
Bootpay.init(supportFragmentManager)
.setPayload(payload)
.setEventListener(object : BootpayEventListener {
override fun onDone(data: String) {
// 발급 완료 — 서버에서 빌링키 조회
Log.d("Bootpay", "빌링키 발급 완료: " + data)
}
override fun onConfirm(data: String): Boolean = true
override fun onCancel(data: String) {
Log.d("Bootpay", "사용자 취소: " + data)
}
override fun onError(data: String) {
Log.e("Bootpay", "발급 오류: " + data)
}
override fun onIssued(data: String) {
Log.d("Bootpay", "가상계좌 발급: " + data)
}
override fun onClose() {
Bootpay.dismiss()
}
})
.requestSubscription()kotlinlet payload = Payload()
payload.clientKey = "[ Client Key ]"
payload.pg = "nicepay"
payload.method = "card_rebill"
payload.orderName = "정기결제 등록"
payload.subscriptionId = "SUB-\(Int(Date().timeIntervalSince1970 * 1000))"
payload.price = 0
let user = User()
user.username = "홍길동"
user.phone = "01012345678"
payload.user = user
let extra = Extra()
extra.subscribeTestPayment = true
payload.extra = extra
Bootpay.requestSubscription(
viewController: self,
payload: payload,
isModal: true,
modalPresentationStyle: .automatic,
animated: true
)
.onDone { data in
// 발급 완료 — 서버에서 빌링키 조회
print("빌링키 발급 완료: \(data)")
}
.onConfirm { data in
return true
}
.onCancel { data in
print("사용자 취소: \(data)")
}
.onError { data in
print("발급 오류: \(data)")
}
.onClose {
print("발급창 닫힘")
}swiftPayload payload = Payload();
payload.clientKey = '[ Client Key ]';
payload.pg = 'nicepay';
payload.method = 'card_rebill';
payload.orderName = '정기결제 등록';
payload.subscriptionId = 'SUB-${DateTime.now().millisecondsSinceEpoch}';
payload.price = 0;
User user = User();
user.username = '홍길동';
user.phone = '01012345678';
payload.user = user;
Extra extra = Extra();
extra.subscribeTestPayment = true;
payload.extra = extra;
Bootpay().requestSubscription(
context: context,
payload: payload,
showCloseButton: false,
onDone: (String data) {
// 발급 완료 — 서버에서 빌링키 조회
print('빌링키 발급 완료: $data');
},
onConfirm: (String data) {
return true;
},
onCancel: (String data) {
print('사용자 취소: $data');
},
onError: (String data) {
print('발급 오류: $data');
},
onClose: () {
print('발급창 닫힘');
Bootpay().dismiss(context);
},
);dart// web/index.html <head>에 JS SDK 스크립트 추가 필수
Payload payload = Payload();
payload.clientKey = '[ Client Key ]';
payload.pg = 'nicepay';
payload.method = 'card_rebill';
payload.orderName = '정기결제 등록';
payload.subscriptionId = 'SUB-${DateTime.now().millisecondsSinceEpoch}';
payload.price = 0;
User user = User();
user.username = '홍길동';
user.phone = '01012345678';
payload.user = user;
Extra extra = Extra();
extra.subscribeTestPayment = true;
extra.openType = 'redirect';
extra.redirectUrl = 'https://yoursite.com/billing/result';
payload.extra = extra;
Bootpay().requestSubscription(
context: context,
payload: payload,
showCloseButton: false,
onDone: (String data) {
// redirect 방식에서는 호출되지 않음
print('빌링키 발급 완료: $data');
},
onCancel: (String data) {
print('사용자 취소: $data');
},
onError: (String data) {
print('발급 오류: $data');
},
onClose: () {
Bootpay().dismiss(context);
},
);dartimport React, { useRef } from 'react';
import { Bootpay } from 'react-native-bootpay-api';
const bootpay = useRef<Bootpay>(null);
const payload = {
client_key: '[ Client Key ]',
pg: 'nicepay',
method: 'card_rebill',
order_name: '정기결제 등록',
subscription_id: 'SUB-' + Date.now(),
price: 0,
user: {
id: 'user_123',
username: '홍길동',
phone: '01012345678',
},
extra: {
subscribe_test_payment: true,
}
};
if (bootpay != null && bootpay.current != null)
bootpay.current.requestSubscription(payload);
<Bootpay ref={bootpay}
client_key={'[ Client Key ]'}
onDone={(data) => {
// 발급 완료 — 서버에서 빌링키 조회
console.log('빌링키 발급 완료', data);
}}
onConfirm={(data) => true}
onCancel={(data) => { console.log('사용자 취소', data); }}
onError={(data) => { console.log('발급 오류', data); }}
onClose={() => { console.log('발급창 닫힘'); }}
/>tsx발급 결과 처리
빌링키 발급이 완료되면 done 이벤트를 전달받아요. 보안상 빌링키는 프론트엔드로 바로 전달되지 않으므로, 백엔드에서 빌링키 조회 API를 호출하여 빌링키를 확인하고 데이터베이스에 저장해야 해요.
백엔드에서 발급하기
가맹점이 직접 카드 정보를 수집하여 백엔드에서 빌링키를 발급하는 방식이에요.
나이스페이먼츠, 페이앱, 웰컴페이먼츠, 토스페이먼츠, 키움페이, 라이트페이
API 정보
https://api.bootpay.co.kr/v2/request/subscribeBasic Auth필수 파라미터
가맹점 결제창에서 수집한 카드 정보와 함께 아래 값을 보내야 해요.
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
pg |
String | 필수 | PG사 코드 (미입력 시 SUBSCRIBE_NEED_PG_METHOD 2300) |
method |
String | 필수 | 반드시 카드자동(REST) 값을 지정 |
subscription_id |
String | 필수 | 가맹점에서 관리하는 고유 구독 번호 (미입력 시 RC_O_ID_BLANK 2005) |
order_name |
String | 필수 | 주문명 (미입력 시 RC_NAME_BLANK 2003) |
card_no |
String | 필수 | 카드번호 (하이픈 없이) |
card_pw |
String | 필수 | 카드 비밀번호 앞 2자리 |
card_identity_no |
String | 필수 | 생년월일 6자리 또는 사업자등록번호 10자리 |
card_expire_year |
String | 필수 | 카드 유효기간 년 2자리 |
card_expire_month |
String | 필수 | 카드 유효기간 월 2자리 (01~12 범위를 벗어나면 RC_INVALID_EXP_MONTH 2084) |
method를 생략하면 서버가 기본값 카드자동(결제창 발급용)으로 처리해요. 이 값은 REST API로 발급할 수 있는 결제수단이 아니라서, 카드 정보를 정확히 보내도 RC_ONLY_REST_API(2058)로 거절돼요. 백엔드 REST 발급에서는 method: '카드자동(REST)'를 반드시 명시해야 해요.
import { Bootpay } from '@bootpay/backend-js'
Bootpay.setConfiguration({
client_key: '[ Client Key ]',
secret_key: '[ Secret Key ]'
})
try {
const response = await Bootpay.requestSubscribeBillingKey({
pg: 'nicepay',
method: '카드자동(REST)',
subscription_id: 'sub_' + Date.now(),
card_no: '5570********1074',
card_pw: '00',
card_identity_no: '900101',
card_expire_year: '25',
card_expire_month: '12',
order_name: '정기결제 등록'
})
// billing_key를 데이터베이스에 저장
console.log(response)
} catch (e) {
console.log(e)
}javascriptfrom bootpay_backend import BootpayBackend
import time
bootpay = BootpayBackend(client_key='CLIENT_KEY', secret_key='SECRET_KEY')
response = bootpay.request_subscribe_billing_key(
pg='nicepay',
method='카드자동(REST)',
subscription_id=f'sub_{int(time.time())}',
card_no='5570********1074',
card_pw='00',
card_identity_no='900101',
card_expire_year='25',
card_expire_month='12',
order_name='정기결제 등록'
)
print(response)python<?php
require_once 'vendor/autoload.php';
use Bootpay\ServerPhp\BootpayApi;
BootpayApi::setClientKeyConfiguration('CLIENT_KEY', 'SECRET_KEY');
$response = BootpayApi::requestSubscribeBillingKey([
'pg' => 'nicepay',
'method' => '카드자동(REST)',
'subscription_id' => 'sub_' . time(),
'card_no' => '5570********1074',
'card_pw' => '00',
'card_identity_no' => '900101',
'card_expire_year' => '25',
'card_expire_month' => '12',
'order_name' => '정기결제 등록'
]);
// billing_key를 데이터베이스에 저장
echo $response['data']['billing_key'];phpimport java.util.HashMap;
import kr.co.bootpay.pg.Bootpay;
import kr.co.bootpay.pg.model.request.Subscribe;
Bootpay bootpay = Bootpay.withClientKey("CLIENT_KEY", "SECRET_KEY");
try {
Subscribe subscribe = new Subscribe();
subscribe.pg = "nicepay";
subscribe.method = "카드자동(REST)";
subscribe.subscriptionId = "sub_" + System.currentTimeMillis();
subscribe.cardNo = "5570********1074";
subscribe.cardPw = "00";
subscribe.cardIdentityNo = "900101";
subscribe.cardExpireYear = "25";
subscribe.cardExpireMonth = "12";
subscribe.orderName = "정기결제 등록";
HashMap<String, Object> response = bootpay.getBillingKey(subscribe);
// billing_key를 데이터베이스에 저장
System.out.println(response.get("billing_key"));
} catch (Exception e) {
e.printStackTrace();
}javarequire 'bootpay'
bootpay = Bootpay::Api.new(client_key: 'CLIENT_KEY', secret_key: 'SECRET_KEY')
response = bootpay.request(
uri: 'request/subscribe',
payload: {
pg: 'nicepay',
method: '카드자동(REST)',
subscription_id: "sub_#{Time.now.to_i}",
card_no: '5570********1074',
card_pw: '00',
card_identity_no: '900101',
card_expire_year: '25',
card_expire_month: '12',
order_name: '정기결제 등록'
}
)
# billing_key를 데이터베이스에 저장
puts response.data['billing_key']rubypackage main
import (
"fmt"
"time"
"github.com/bootpay/backend-go/bootpay"
)
func main() {
api := bootpay.NewAPIWithClientKey("CLIENT_KEY", "SECRET_KEY", nil, "")
payload := bootpay.BillingKeyPayload{
Pg: "nicepay",
Method: "카드자동(REST)",
SubscriptionId: fmt.Sprintf("sub_%d", time.Now().UnixMilli()),
CardNo: "5570********1074",
CardPw: "00",
CardIdentityNo: "900101",
CardExpireYear: "25",
CardExpireMonth: "12",
OrderName: "정기결제 등록",
}
response, err := api.GetBillingKey(payload)
if err != nil {
fmt.Println(err)
return
}
// billing_key를 데이터베이스에 저장
fmt.Println(response)
}gousing Bootpay;
using Bootpay.models;
var api = BootpayApi.WithClientKey("CLIENT_KEY", "SECRET_KEY");
var subscribe = new Subscribe
{
pg = "nicepay",
method = "카드자동(REST)",
subscriptionId = $"sub_{DateTimeOffset.Now.ToUnixTimeMilliseconds()}",
cardNo = "5570********1074",
cardPw = "00",
cardIdentityNo = "900101",
cardExpireYear = "25",
cardExpireMonth = "12",
orderName = "정기결제 등록"
};
var response = await api.GetBillingKey(subscribe);
// billing_key를 데이터베이스에 저장
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(content);csharp고객의 카드 정보(카드번호, 비밀번호, 유효기간 등)는 절대 데이터베이스에 저장하면 안 돼요. 여전법 위반이에요.
REST 발급 응답
백엔드 REST 발급은 발급이 끝나면 응답에 billing_key가 바로 들어와요. 프론트엔드 결제창 발급과 달리 빌링키 조회 API를 다시 호출할 필요가 없어요.
{
"receipt_id": "6261104e1fc19202e6f9420e",
"subscription_id": "sub_1705289520000",
"pg": "나이스페이먼츠",
"method": "카드자동(REST)",
"method_symbol": "card_rebill_rest",
"method_origin": "카드자동(REST)",
"method_origin_symbol": "card_rebill_rest",
"published_at": "2025-01-15T14:32:00+09:00",
"requested_at": "2025-01-15T14:31:52+09:00",
"status_locale": "빌링키발급완료",
"status": 11,
"billing_key": "615d00f0197c300036b4fef5",
"billing_data": {
"card_company": "하나카드",
"card_no": "5570-****-****-1074",
"card_company_code": "046",
"card_type": 0,
"card_hash": "f5b2a..."
},
"billing_expire_at": "2026-01-01T00:00:00+09:00"
}json| 파라미터 | 타입 | 설명 |
|---|---|---|
| receipt_id | String | 발급 요청의 영수증 ID |
| subscription_id | String | 요청 시 넣은 가맹점 고유 구독 번호 |
| pg | String | PG사명 |
| method | String | 결제수단 한글명 |
| method_symbol | String | 결제수단 영문 심볼 |
| method_origin | String | 원 결제수단 한글명 |
| method_origin_symbol | String | 원 결제수단 영문 심볼 |
| published_at | Date | 빌링키 발급 시각 (ISO 8601) |
| requested_at | Date | 발급 요청 시각 (ISO 8601) |
| status_locale | String | 발급 상태 한글 ("빌링키발급완료") |
| status | Number | 발급 상태. 11이면 발급 완료 |
| billing_key | String | 발급된 빌링키 — 반드시 데이터베이스에 저장 |
| billing_data | Object | 카드 표시 정보 (card_company, card_no, card_company_code, card_type, card_hash) |
| billing_expire_at | Date | 빌링키 만료일. REST 카드 발급은 입력한 카드 유효기간 + 1개월로 계산돼요 |
| receipt_data | Object | price를 0보다 크게 보내 발급과 동시에 결제까지 진행한 경우에만 포함되는 결제 정보 |
에러 코드
인증·권한 관련 에러는 에러 코드표를 참고해요.
| 코드 | 메시지 | 대처 방법 |
|---|---|---|
SUBSCRIBE_NEED_PG_METHOD (2300) |
pg와 method 정보가 필수이다 | pg와 method 파라미터를 입력해요 |
RC_ONLY_REST_API (2058) |
현재 요청한 결제 수단은 REST API로 요청이 가능한 결제 수단이 아니다 | 백엔드 REST 발급은 method를 카드자동(REST)로 보내야 해요 |
RC_INVALID_EXP_MONTH (2084) |
카드 만료월 정보를 다시 확인한다 (01월~12월) | card_expire_month를 01~12 범위로 입력해요 |
RC_NAME_BLANK (2003) |
상품명을 입력한다 | order_name 값을 입력해요 |
RC_O_ID_BLANK (2005) |
order_id 값을 입력한다 | subscription_id 또는 order_id를 입력해요 |
RC_PRICE_LEAST_LT (2004) |
100원보다 큰 금액을 결제할 수 있다 | price를 100원 이상으로 설정해요 |
RC_NOT_SUBSCRIBE (2057) |
정기결제 요청 정보가 아니다 | 정기결제 요청 파라미터를 확인해요 |
RC_PROVIDER_NOT_ALLOW (2104) |
가맹점이 탈퇴/차단 상태 | 부트페이 관리자에서 가맹점 상태를 확인해요 |
RC_PROVIDER_PAYMENT_NOT_ALLOW (2102) |
서비스 이용료 미납으로 결제 불가 | 부트페이 서비스 이용료를 납부해요 |
SUBSCRIBE_CARD_NO_BLANK (2311) |
카드 번호를 입력한다 | card_no 값을 입력해요 |
SUBSCRIBE_CARD_PW_BLANK (2312) |
카드 비밀번호 앞 2자리를 입력한다 | card_pw 값을 입력해요 |
SUBSCRIBE_CARD_IDENTITY_BLANK (2313) |
생년월일/사업자등록번호를 입력한다 | card_identity_no 값을 입력해요 |
SUBSCRIBE_CARD_EX_YEAR_BLANK (2314) |
카드 만료 년도를 입력한다 | card_expire_year 값을 입력해요 |
SUBSCRIBE_CARD_EX_MONTH_BLANK (2315) |
카드 만료 월을 입력한다 | card_expire_month 값을 입력해요 |
SUBSCRIBE_REQUEST_FAILED (2301) |
빌링키 발급 요청 실패 | 카드 정보를 확인 후 재시도해요 |
RC_RESOURCE_NOT_CONFIG (2017) |
결제수단이 허가/설정되지 않았다 | 부트페이 관리자에서 정기결제 설정을 확인해요 |
SUBSCRIBE_PUBLISH_NOT_READY (2303) |
빌링키 발급 대기 상태가 아니다 | 발급 요청을 처음부터 다시 진행해요 |
SUBSCRIBE_PUBLISH_M_NOT_FOUND (2305) |
빌링키 발급 기능이 없는 PG | 빌링키 발급을 지원하는 PG를 사용해요 |
SUBSCRIBE_PUBLISH_FAILED (2304) |
빌링키 발급 처리 중 서버 오류가 발생했다 | 잠시 후 재시도하고, 계속 발생하면 부트페이 관리자에 문의해요 |
SUBSCRIBE_ALREADY_PUBLISHED (2319) |
이미 빌링키를 발급받은 진행건이다 | 같은 receipt_id로 중복 발급을 요청한 경우예요. 발급된 빌링키를 그대로 사용해요 |
다음 단계
더 읽을거리
- 자동결제와 구독을 구분하는 기준 — 빌링키 기반 결제 모델 선택 조건
- 구독 도입 전에 설계할 항목 — 요금제·청구 주기·실패 처리 기준
