사전에 읽어볼만한 문서는 아래와 같아요.
- 환경설정 — 관리자 가입, 프로젝트 생성, PG 활성화, 샌드박스 모드까지 한 번에
- 연동키 발급 — Client Key·Secret Key 발급과 권한·만료 설정
- 결제 연동 시작하기 — SDK 호출 → 서버 검증·승인 → 결과 처리 전체 흐름
PG 가맹 자체에 대한 의사결정(어떤 PG를 고를지, 사업군별 수수료, 가맹 심사 진행 방식)은 블로그에서 다뤄요.
핵심 요약
- 결제 요청은 프론트엔드의
Bootpay.requestPayment()호출로 시작해요. pg·method값으로 특정 PG사와 결제수단을 지정할 수 있고, 미지정 시 활성화된 결제수단을 고르는 통합결제창이 열려요.- 카드사 인증이 완료되면
confirm이 호출돼요. 이 시점은 결제 완료가 아니라 승인 직전이므로, 백엔드에서receipt_id로 주문 상태·금액을 확인한 뒤 결제를 승인해야 해요. - 운영 환경에서는 백엔드가 승인 API를 호출하는 서버 승인(
extra.separately_confirmed: true) 방식을 권장해요. 단, PG사에 따라 서버 승인을 지원하지 않아 클라이언트에서Bootpay.confirm()을 호출해야 할 수 있어요. - 가상계좌는 결제창에서 계좌 발급까지만 끝나요. 실제 입금 완료는 웹훅으로 받아 백엔드에서 확정해야 해요.
결제 흐름 한눈에
이 문서의 핵심 흐름은 아래와 같아요. 결제창을 띄우는 일은 프론트엔드가 담당하고, 카드사 인증이 끝나면 confirm이 전달돼요. 주문 확정은 백엔드가 receipt_id 기준으로 검증·승인한 뒤 처리해야 해요.
이 문서에서는 권장 방식인 서버 승인(extra.separately_confirmed: true)을 기준으로 설명해요. 카드사 인증이 완료되면 confirm이 호출되고, 백엔드가 승인 직전에 금액·주문·재고를 한 번 더 검증한 뒤 결제를 승인해요.
confirm이나 done 같은 클라이언트 이벤트만으로 주문을 확정하면 안 돼요. 웹훅을 함께 설정해 백엔드에서 결과를 한 번 더 받아야 해요.
1SDK 설치
결제창 연동은 클라이언트 SDK와 서버 SDK를 나눠 설치해요.
- 클라이언트 SDK: 결제창 호출, 구매자 입력, 결제 이벤트 수신
- 서버 SDK: 결제 조회, 서버 승인, 취소, 영수증 조회, 웹훅 후속 처리
클라이언트 SDK
npm install @bootpay/client-jsbash<script src="https://js.bootpay.co.kr/bootpay-5.3.0.min.js"></script>htmldependencies {
implementation 'io.github.bootpay:android:5.1.1'
}target 'YourApp' do
pod 'Bootpay', '~> 5.1.1'
endrubyflutter pub add bootpaybashnpm install react-native-bootpay-api
cd ios && pod install && cd ..bash서버 SDK
npm install @bootpay/backend-jsbashpip install bootpay-backendbashcomposer require bootpay/server-phpbashdependencies {
implementation 'io.github.bootpay:backend:3.0.5'
}gem install bootpaybashgo get github.com/bootpay/backend-go/v2bashdotnet add package Bootpaybash설치 후 호출에 사용할 Client Key가 아직 없다면 연동키 발급을 먼저 진행해요.
2결제 요청
코드 예제
아래 예제는 모두 extra.separately_confirmed: true 기준이에요. 구매자가 카드사 인증을 마치면 confirm이 호출되고, 그 다음 단계에서 결제를 승인해요.
import { Bootpay } from '@bootpay/client-js'
await Bootpay.requestPayment({
client_key: '[ Client Key ]',
price: 50000,
order_name: '나이키 운동화 외 2건',
order_id: 'order_' + Date.now(), // 테스트용. 실제로는 서버에서 생성한 주문번호 사용
pg: 'nicepay',
method: 'card',
user: {
username: '홍길동',
phone: '01012345678',
email: 'user@example.com'
},
extra: {
separately_confirmed: true,
open_type: 'redirect',
// Web redirect에서는 인증 완료 후 이 서버 URL이 confirm을 먼저 받는다.
redirect_url: 'https://yoursite.com/order/result'
}
})javascript<script src="https://js.bootpay.co.kr/bootpay-5.3.0.min.js"></script>
<script>
Bootpay.requestPayment({
client_key: '[ Client Key ]',
price: 50000,
order_name: '나이키 운동화 외 2건',
order_id: 'order_' + Date.now(), // 테스트용. 실제로는 서버에서 생성한 주문번호 사용
pg: 'nicepay',
method: 'card',
user: {
username: '홍길동',
phone: '01012345678'
},
extra: {
separately_confirmed: true,
open_type: 'redirect',
// Web redirect에서는 인증 완료 후 이 서버 URL이 confirm을 먼저 받는다.
redirect_url: 'https://yoursite.com/order/result'
}
})
</script>html// UIViewController 내부에서 호출
let payload = Payload()
payload.clientKey = "[ Client Key ]"
payload.price = 50000
payload.orderName = "나이키 운동화 외 2건"
payload.orderId = "order_\(Int(Date().timeIntervalSince1970))"
payload.pg = "나이스페이"
payload.method = "카드"
let user = BootUser()
user.username = "홍길동"
user.phone = "01012345678"
user.email = "user@example.com"
payload.user = user
let extra = BootExtra()
extra.separatelyConfirmed = true
payload.extra = extra
Bootpay.requestPayment(viewController: self, payload: payload)
.onIssued { data in
// 가상계좌는 issued에서 계좌 발급 정보를 받는다.
// 주문은 입금 대기 상태로 저장하고, 실제 확정은 웹훅에서 처리해야 한다.
self.savePendingVbankOrder(data)
}
.onConfirm { data in
// 앱 SDK에서는 인증 완료 후 confirm 콜백이 먼저 호출된다.
// data.receipt_id를 백엔드 승인 API로 보내고, 앱에서 바로 주문 완료 처리하면 안 돼요.
self.requestServerApproval(data)
return false
}
.onDone { data in
// 서버 승인(separately_confirmed: true)에서는 호출되지 않는다.
// 클라이언트 자동 승인 방식에서만 결제 완료 후 호출된다.
}
.onCancel { data in self.keepOrderUnpaid(data) }
.onError { data in self.showPaymentError(data) }
.onClose { self.resetPaymentUi() }swift// Activity 내부에서 호출
val user = BootUser()
.setUsername("홍길동")
.setPhone("01012345678")
.setEmail("user@example.com")
val extra = BootExtra()
.setSeparatelyConfirmed(true)
val payload = Payload()
.setClientKey("[ Client Key ]")
.setOrderName("나이키 운동화 외 2건")
.setOrderId("order_${System.currentTimeMillis()}")
.setPg("나이스페이")
.setMethod("카드")
.setPrice(50000.0)
.setUser(user)
.setExtra(extra)
Bootpay.init(supportFragmentManager)
.setPayload(payload)
.setEventListener(object : BootpayEventListener {
override fun onConfirm(data: String): Boolean {
// 앱 SDK에서는 인증 완료 후 confirm 콜백이 먼저 호출된다.
// data 안의 receipt_id를 백엔드 승인 API로 보내고, 앱에서 바로 주문 완료 처리하면 안 돼요.
requestServerApproval(data)
return false
}
override fun onCancel(data: String) {
// 사용자가 결제창을 닫은 경우. 주문은 미결제 상태로 유지해야 한다.
keepOrderUnpaid(data)
}
override fun onError(data: String) {
// 결제 요청·승인 과정에서 오류가 발생한 경우.
logPaymentError(data)
showPaymentError(data)
}
override fun onClose() { Bootpay.removePaymentWindow() }
override fun onIssued(data: String) {
// 가상계좌는 issued에서 계좌 발급 정보를 받는다.
// 주문은 입금 대기 상태로 저장하고, 실제 확정은 웹훅에서 처리해야 한다.
savePendingVbankOrder(data)
}
override fun onDone(data: String) {
// 서버 승인(separately_confirmed: true)에서는 호출되지 않는다.
// 클라이언트 자동 승인 방식에서만 결제 완료 후 호출된다.
}
})
.requestPayment()kotlin// State 안에서 호출
final payload = Payload()
..clientKey = '[ Client Key ]'
..price = 50000
..orderName = '나이키 운동화 외 2건'
..orderId = 'order_${DateTime.now().millisecondsSinceEpoch}'
..pg = '나이스페이'
..method = '카드'
..user = (User()
..username = '홍길동'
..phone = '01012345678'
..email = 'user@example.com')
..extra = (Extra()
..separatelyConfirmed = true);
Bootpay().requestPayment(
context: context,
payload: payload,
onConfirm: (data) {
// 앱 SDK에서는 인증 완료 후 confirm 콜백이 먼저 호출된다.
// data의 receipt_id를 백엔드 승인 API로 보내고, 앱에서 바로 주문 완료 처리하면 안 돼요.
requestServerApproval(data);
return false;
},
onCancel: (data) => debugPrint('cancel: $data'),
onError: (data) => debugPrint('error: $data'),
onIssued: (data) {
// 가상계좌는 issued에서 계좌 발급 정보를 받는다.
// 주문은 입금 대기 상태로 저장하고, 실제 확정은 웹훅에서 처리해야 한다.
savePendingVbankOrder(data);
},
onDone: (data) {
// 서버 승인(separatelyConfirmed: true)에서는 호출되지 않는다.
// 클라이언트 자동 승인 방식에서만 결제 완료 후 호출된다.
},
onClose: () => resetPaymentUi(),
);dart// web/index.html <head>에 JS SDK 스크립트 추가 필수:
// <script src="https://js.bootpay.co.kr/bootpay-5.3.0.min.js"></script>
Payload payload = Payload();
payload.clientKey = '[ Client Key ]';
payload.price = 50000;
payload.orderName = '나이키 운동화 외 2건';
payload.orderId = DateTime.now().millisecondsSinceEpoch.toString();
payload.pg = 'nicepay';
payload.method = 'card';
User user = User();
user.username = '홍길동';
user.phone = '01012345678';
user.email = 'user@example.com';
payload.user = user;
// Flutter Web에서는 redirect 방식 권장
payload.extra = Extra();
payload.extra?.separatelyConfirmed = true;
payload.extra?.openType = 'redirect';
// Web redirect에서는 인증 완료 후 이 서버 URL이 confirm을 먼저 받는다.
payload.extra?.redirectUrl = 'https://yoursite.com/order/result';
Bootpay().requestPayment(
context: context,
payload: payload,
onIssued: (data) {
// 가상계좌는 issued에서 계좌 발급 정보를 받는다.
savePendingVbankOrder(data);
},
onDone: (data) {
// 서버 승인(separatelyConfirmed: true)에서는 호출되지 않는다.
// 클라이언트 자동 승인 방식에서만 결제 완료 후 호출된다.
},
onCancel: (data) => keepOrderUnpaid(data),
onError: (data) => showPaymentError(data),
onClose: () => resetPaymentUi(),
);dartimport React, { useRef } from 'react'
import Bootpay from 'react-native-bootpay-api'
export default function OrderScreen() {
const bootpay = useRef(null)
const requestPayment = () => {
const payload = {
pg: '나이스페이',
method: '카드',
order_name: '나이키 운동화 외 2건',
order_id: `order_${Date.now()}`,
price: 50000
}
const user = {
username: '홍길동',
phone: '01012345678',
email: 'user@example.com'
}
const extra = {
separately_confirmed: true,
app_scheme: 'bootpaySample'
}
bootpay.current?.requestPayment(payload, [], user, extra)
}
return (
<Bootpay
ref={bootpay}
client_key={'[ Client Key ]'}
onConfirm={(data) => {
// 앱 SDK에서는 인증 완료 후 confirm 콜백이 먼저 호출된다.
// data.receipt_id를 백엔드 승인 API로 보내고, 앱에서 바로 주문 완료 처리하면 안 돼요.
requestServerApproval(data)
return false
}}
onIssued={(data) => {
// 가상계좌는 issued에서 계좌 발급 정보를 받는다.
// 주문은 입금 대기 상태로 저장하고, 실제 확정은 웹훅에서 처리해야 한다.
savePendingVbankOrder(data)
}}
onDone={(data) => {
// 서버 승인(separately_confirmed: true)에서는 호출되지 않는다.
// 클라이언트 자동 승인 방식에서만 결제 완료 후 호출된다.
}}
onCancel={(data) => console.log('cancel', data)}
onError={(data) => console.log('error', data)}
onClose={() => resetPaymentUi()}
/>
)
}jsx요청 파라미터
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
client_key |
String | 필수 | Client Key. 부트페이 관리자 > 개발자 설정 > API 연동키에서 확인 |
price |
Integer | 필수 | 결제 금액 (원 단위) |
order_name |
String | 필수 | 주문명 (예: "나이키 운동화 외 2건") |
order_id |
String | 필수 | 가맹점 고유 주문번호. 유니크한 값으로 생성하고 DB에 저장 권장 |
pg |
String | 선택 | PG사 코드 (예: "nicepay", "kcp"). 미입력 시 통합결제창으로 진행 |
method |
String / Array | 선택 | 결제수단 코드 (예: "card", "bank", "phone", "easy"). Array로 입력 시 통합결제창으로 진행 |
tax_free |
Integer | 선택 | 비과세 금액 (기본값: 0). 면세인 경우 price와 동일하게 입력 |
metadata |
Object | 선택 | 결제 요청 시 전달할 추가 데이터. 결제 결과에 그대로 반환됨 |
user |
Object | 선택 | 구매자 정보. 일부 PG는 필수 |
user.id |
String | 선택 | 회원 아이디 |
user.username |
String | 선택 | 회원 이름. 휴대폰결제·가상계좌·본인인증 시 선입력 |
user.phone |
String | 선택 | 연락 가능한 전화번호. 휴대폰결제·본인인증 시 선입력. 페이앱 결제 시 필수 |
user.email |
String | 선택 | 회원 이메일 주소 |
user.addr |
String | 선택 | 배송지 주소. 일부 에스크로 결제 시 필요하다 |
items |
Array | 선택 | 상품 정보 목록. qty × price 합계가 price와 일치해야 함. 페이코 직연동 시 필수 |
items[].id |
String | 선택 | 상품 고유 아이디 |
items[].name |
String | 선택 | 상품명 |
items[].qty |
Integer | 선택 | 수량 (0보다 커야 함) |
items[].price |
Integer | 선택 | 상품 가격 (0보다 커야 함) |
extra |
Object | 선택 | 추가 결제 옵션. 아래 상세 참고 |
extra 옵션 상세
UI 노출
| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| extra.open_type | String | iframe |
결제창 진행방식. redirect, iframe, popup. 오픈 타입 상세 |
| extra.locale | String | ko |
언어 설정. ko, en |
| extra.show_close_button | Boolean | false |
iFrame 결제 시 닫기 버튼 활성화 (SDK 4.1.5 이상) |
| extra.display_cash_receipt | Boolean | true |
PG 현금영수증 입력창 표시 여부 |
| extra.display_success_result | Boolean | false |
결제 완료 결과를 부트페이 제공 페이지로 표시 |
| extra.display_error_result | Boolean | false |
오류 발생 시 부트페이 제공 결과 UI에 에러 내용 표시 |
| extra.offer_period | String | - | 결제창에 노출되는 제공기간 정보 |
결제수단 제약
| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| extra.card_quota | String | - | 5만원 이상 카드 할부 개월수. "0,2,3,4,5,6,7,8,9,10,11,12" |
| extra.escrow | Boolean | - | 에스크로 결제인 경우 true |
| extra.enable_card_companies | Array | null | 노출할 카드사 선택. 예: ["국민", "신한"] (KCP, 이니시스, 웰컴페이먼츠, 나이스페이만 가능) |
| extra.except_card_companies | Array | null | 제외할 카드사. 예: ["국민", "신한"] |
| extra.enable_easy_payments | Array | null | 노출할 간편결제. 예: ["카카오페이", "페이코"] (웰컴페이먼츠만 가능) |
| extra.easy_payment_method | String | 카드 |
네이버페이 결제 시 포인트/카드 선택 (나이스페이먼츠만 가능) |
| extra.deposit_expiration | String | 오늘 + 3일 | 가상계좌 입금 만료일. yyyy-MM-dd HH:mm:ss |
| extra.test_deposit | Boolean | false |
샌드박스 가상계좌 테스트 시 true면 발급과 동시에 모의입금 |
실패·웹훅
| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| extra.common_event_webhook | Boolean | false |
결제창 닫힘·만료 이벤트를 웹훅으로 수신 |
| extra.enable_error_webhook | Boolean | false |
결제 승인 실패 이벤트를 웹훅으로 수신 |
| extra.separately_confirmed | Boolean | false |
true: 인증 결과로 confirm을 받고 4번 결제 승인 요청 단계에서 승인 처리 |
시간·복귀
| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| extra.timeout | Integer | - | 결제 만료 시간 (분) |
| extra.redirect_url | String | - | open_type이 redirect인 경우 결제 결과를 받을 URL |
| extra.app_scheme | String | - | iOS 결제 후 앱 복귀를 위한 스키마 |
3인증 결과 수신
구매자가 카드사 인증을 마치면 인증 결과로 confirm이 호출돼요. 이 단계는 인증 결과를 받는 단계이지, 아직 결제 승인이나 주문 확정이 아니에요.
# Web SDK에서 open_type: 'redirect'를 쓰는 경우의 인증 결과 수신 방식이다.
# Web redirect에서는 confirm 콜백을 프론트엔드에서 먼저 받지 않는다.
# 구매자 인증이 완료되면 Bootpay가 아래 서버 URL로 사용자를 이동시키며, 서버가 query로 confirm을 받는다.
GET /order/result
query:
event: confirm
receipt_id: 6244f60c1fc19202e42e8c4e
order_id: order_1648686604470
# 서버는 아직 주문을 확정하면 안 돼요.
# receipt_id로 주문 금액·상태·재고를 검증한 뒤 다음 단계에서 결제 승인 요청을 해야 한다.import Foundation
import UIKit
final class OrderViewController: UIViewController {
func requestPayment() {
let payload = Payload()
payload.clientKey = "[ Client Key ]"
payload.price = 50000
payload.orderName = "나이키 운동화 외 2건"
payload.orderId = "order_\(Int(Date().timeIntervalSince1970))"
payload.pg = "나이스페이"
payload.method = "카드"
let extra = BootExtra()
extra.separatelyConfirmed = true
payload.extra = extra
Bootpay.requestPayment(viewController: self, payload: payload)
.onIssued { data in
// 가상계좌는 issued에서 계좌 발급 정보를 받는다.
// 실제 결제 확정은 입금 웹훅에서 처리해야 한다.
self.savePendingVbankOrder(data)
}
.onConfirm { data in
// iOS 앱은 인증 완료 후 confirm을 앱 콜백으로 먼저 받는다.
// 이 자리에서 주문 완료 처리하면 안 되고, receipt_id를 서버 승인 API로 보내야 한다.
self.requestServerApproval(
receiptId: data["receipt_id"] as? String,
orderId: payload.orderId
)
return false
}
.onDone { data in
// 서버 승인(separately_confirmed: true)에서는 호출되지 않는다.
// 클라이언트 자동 승인 방식에서만 결제 완료 후 호출된다.
}
.onCancel { _ in self.showCancel() }
.onError { data in self.showError(data) }
.onClose { self.resetPaymentUi() }
}
private func requestServerApproval(receiptId: String?, orderId: String?) {
guard let receiptId, let orderId else { return }
var request = URLRequest(url: URL(string: "https://yoursite.com/order/result")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(withJSONObject: [
"event": "confirm",
"receipt_id": receiptId,
"order_id": orderId
])
URLSession.shared.dataTask(with: request) { _, _, _ in
// 서버가 금액·주문 상태·재고를 검증한 뒤 Bootpay 승인 API를 호출한다.
// 앱은 서버 응답을 받은 뒤 완료 화면으로 이동해야 한다.
}.resume()
}
}swiftimport java.io.IOException
import okhttp3.Call
import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.json.JSONObject
class OrderActivity : AppCompatActivity() {
private val httpClient = OkHttpClient()
fun requestPayment() {
val orderId = "order_${System.currentTimeMillis()}"
val extra = BootExtra().setSeparatelyConfirmed(true)
val payload = Payload()
.setClientKey("[ Client Key ]")
.setOrderName("나이키 운동화 외 2건")
.setOrderId(orderId)
.setPg("나이스페이")
.setMethod("카드")
.setPrice(50000.0)
.setExtra(extra)
Bootpay.init(supportFragmentManager)
.setPayload(payload)
.setEventListener(object : BootpayEventListener {
override fun onConfirm(data: String): Boolean {
// Android 앱은 인증 완료 후 confirm을 앱 콜백으로 먼저 받는다.
// data에서 receipt_id를 꺼내 서버 승인 API로 보내고, 앱에서는 승인 결과만 화면에 반영한다.
requestServerApproval(data, orderId)
return false
}
override fun onCancel(data: String) { showCancel() }
override fun onError(data: String) { showError(data) }
override fun onClose() { Bootpay.removePaymentWindow() }
override fun onIssued(data: String) {
// 가상계좌는 issued에서 계좌 발급 정보를 받는다.
// 실제 결제 확정은 입금 웹훅에서 처리해야 한다.
savePendingVbankOrder(data)
}
override fun onDone(data: String) {
// 서버 승인(separately_confirmed: true)에서는 호출되지 않는다.
// 클라이언트 자동 승인 방식에서만 결제 완료 후 호출된다.
}
})
.requestPayment()
}
private fun requestServerApproval(confirmData: String, orderId: String) {
val receiptId = JSONObject(confirmData).getString("receipt_id")
val body = JSONObject()
.put("event", "confirm")
.put("receipt_id", receiptId)
.put("order_id", orderId)
.toString()
.toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url("https://yoursite.com/order/result")
.post(body)
.build()
httpClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
showError(e.message ?: "서버 승인 요청 실패")
}
override fun onResponse(call: Call, response: Response) {
response.use {
// 서버가 금액·주문 상태·재고를 검증한 뒤 Bootpay 승인 API를 호출한다.
// 앱은 서버 응답을 받은 뒤 완료 화면으로 이동해야 한다.
}
}
})
}
}kotlinimport 'dart:convert';
import 'package:http/http.dart' as http;
class OrderPage extends StatefulWidget {
const OrderPage({super.key});
@override
State<OrderPage> createState() => _OrderPageState();
}
class _OrderPageState extends State<OrderPage> {
void requestPayment() {
final orderId = 'order_${DateTime.now().millisecondsSinceEpoch}';
final payload = Payload()
..clientKey = '[ Client Key ]'
..price = 50000
..orderName = '나이키 운동화 외 2건'
..orderId = orderId
..pg = '나이스페이'
..method = '카드'
..extra = (Extra()..separatelyConfirmed = true);
Bootpay().requestPayment(
context: context,
payload: payload,
onConfirm: (data) {
// Flutter 앱은 인증 완료 후 confirm을 onConfirm으로 먼저 받는다.
// receipt_id를 서버 승인 API로 보내고, 서버 응답 전에는 주문 완료로 표시하면 안 돼요.
requestServerApproval(data, orderId);
return false;
},
onCancel: (data) => showCancel(),
onError: (data) => showError(data),
onIssued: (data) {
// 가상계좌는 issued에서 계좌 발급 정보를 받는다.
// 실제 결제 확정은 입금 웹훅에서 처리해야 한다.
savePendingVbankOrder(data);
},
onDone: (data) {
// 서버 승인(separatelyConfirmed: true)에서는 호출되지 않는다.
// 클라이언트 자동 승인 방식에서만 결제 완료 후 호출된다.
},
onClose: () => resetPaymentUi(),
);
}
Future<void> requestServerApproval(String data, String orderId) async {
final confirm = jsonDecode(data) as Map<String, dynamic>;
final response = await http.post(
Uri.parse('https://yoursite.com/order/result'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'event': 'confirm',
'receipt_id': confirm['receipt_id'],
'order_id': orderId,
}),
);
// 서버가 금액·주문 상태·재고를 검증한 뒤 Bootpay 승인 API를 호출한다.
// 앱은 서버 응답을 받은 뒤 완료 화면으로 이동해야 한다.
if (response.statusCode != 200) showError(response.body);
}
}dartclass WebOrderPage extends StatelessWidget {
const WebOrderPage({super.key});
void requestPayment(BuildContext context) {
final payload = Payload()
..clientKey = '[ Client Key ]'
..price = 50000
..orderName = '나이키 운동화 외 2건'
..orderId = 'order_${DateTime.now().millisecondsSinceEpoch}'
..pg = 'nicepay'
..method = 'card'
..extra = (Extra()
..separatelyConfirmed = true
..openType = 'redirect'
..redirectUrl = 'https://yoursite.com/order/result');
// Flutter Web은 Web redirect와 동일하게 인증 완료 후 서버 URL이 confirm을 먼저 받는다.
Bootpay().requestPayment(
context: context,
payload: payload,
onIssued: (data) {
// 가상계좌는 issued에서 계좌 발급 정보를 받는다.
savePendingVbankOrder(data);
},
onDone: (data) {
// 서버 승인(separatelyConfirmed: true)에서는 호출되지 않는다.
// 클라이언트 자동 승인 방식에서만 결제 완료 후 호출된다.
},
onCancel: (data) => keepOrderUnpaid(data),
onError: (data) => showPaymentError(data),
onClose: () => resetPaymentUi(),
);
}
}dartimport React, { useRef } from 'react'
import Bootpay from 'react-native-bootpay-api'
export default function OrderScreen() {
const bootpay = useRef(null)
const orderId = useRef(`order_${Date.now()}`)
const requestPayment = () => {
const payload = {
pg: '나이스페이',
method: '카드',
order_name: '나이키 운동화 외 2건',
order_id: orderId.current,
price: 50000
}
const extra = { separately_confirmed: true, app_scheme: 'bootpaySample' }
bootpay.current?.requestPayment(payload, [], {}, extra)
}
const requestServerApproval = async (data) => {
// React Native는 인증 완료 후 confirm을 앱 콜백으로 먼저 받는다.
// data.receipt_id를 서버 승인 API로 보내고, 앱에서 직접 승인 완료 처리하면 안 돼요.
await fetch('https://yoursite.com/order/result', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: 'confirm',
receipt_id: data.receipt_id,
order_id: orderId.current
})
})
}
return (
<Bootpay
ref={bootpay}
client_key={'[ Client Key ]'}
onConfirm={(data) => {
requestServerApproval(data)
return false
}}
onIssued={(data) => {
// 가상계좌는 issued에서 계좌 발급 정보를 받는다.
// 실제 결제 확정은 입금 웹훅에서 처리해야 한다.
savePendingVbankOrder(data)
}}
onDone={(data) => {
// 서버 승인(separately_confirmed: true)에서는 호출되지 않는다.
// 클라이언트 자동 승인 방식에서만 결제 완료 후 호출된다.
}}
onCancel={(data) => console.log('cancel', data)}
onError={(data) => console.log('error', data)}
onClose={() => resetPaymentUi()}
/>
)
}jsx4결제 승인 요청
인증 결과(confirm) 다음 단계는 결제 승인 요청이에요. 이 문서에서는 운영 환경에서 우선 권장하는 서버 승인 흐름을 기준으로 설명해요. 서버는 승인 API를 호출하기 전에 주문 기준값을 검증하고, 승인 응답까지 확인한 뒤 주문을 확정해야 해요.
서버 승인 API 호출
백엔드가 Bootpay 승인 API로 결제 승인 요청을 해요. 이 방식은 주문 검증과 결제 승인을 서버 흐름 안에서 이어서 처리할 수 있어요.
import { Bootpay } from '@bootpay/backend-js'
Bootpay.setConfiguration({
client_key: '[ Client Key ]',
secret_key: '[ Secret Key ]'
})
app.all('/order/result', async (req, res) => {
// Web redirect는 인증 완료 후 GET query로 confirm이 들어온다.
// App SDK는 onConfirm에서 같은 URL로 POST body를 보내면 된다.
const input = req.method === 'GET' ? req.query : req.body
const event = input.event || 'confirm'
const { receipt_id, order_id } = input
if (event === 'cancel') {
return res.render('payment/cancel')
}
if (event === 'error') {
return res.render('payment/fail', { message: '결제 중 오류가 발생했다.' })
}
if (event !== 'confirm' || !receipt_id || !order_id) {
return res.render('payment/fail', { message: '결제 결과를 확인할 수 없다.' })
}
const order = await db.orders.findById(order_id)
if (!order || order.status !== 'pending') {
return res.render('payment/fail', { message: '승인할 수 없는 주문' })
}
if (!checkInventory(order)) {
return res.render('payment/fail', { message: '재고가 부족하다.' })
}
const confirmed = await Bootpay.confirmPayment(receipt_id)
if (confirmed.status !== 1 || confirmed.price !== order.price) {
return res.render('payment/fail', { message: '결제 조회에 실패했다.' })
}
await db.orders.update(
{ status: 'done', bootpay_receipt_id: receipt_id },
{ where: { id: order_id } }
)
return res.render('payment/complete', { order_id })
})javascript@app.route('/order/result', methods=['GET', 'POST'])
def payment_result():
# Web redirect는 인증 완료 후 GET query, App SDK는 POST JSON으로 confirm을 보낸다.
data = request.args if request.method == 'GET' else request.get_json()
event = data.get('event', 'confirm')
receipt_id = data.get('receipt_id')
order_id = data.get('order_id')
if event == 'cancel':
return render_template('payment/cancel.html')
if event == 'error':
return render_template('payment/fail.html', message='결제 중 오류가 발생했다.')
if event != 'confirm' or not receipt_id or not order_id:
return render_template('payment/fail.html', message='결제 결과를 확인할 수 없다.')
order = Order.find(order_id)
if not order or order.status != 'pending':
return render_template('payment/fail.html', message='승인할 수 없는 주문')
if not check_inventory(order):
return render_template('payment/fail.html', message='재고가 부족하다.')
confirmed = bootpay.confirm_payment(receipt_id)
if confirmed.get('status') != 1 or confirmed.get('price') != order.price:
return render_template('payment/fail.html', message='결제 조회에 실패했다.')
order.update(status='done', bootpay_receipt_id=receipt_id)
return render_template('payment/complete.html', order_id=order_id)python// Web redirect는 인증 완료 후 GET query, App SDK는 POST JSON으로 confirm을 보낸다.
$input = $_SERVER['REQUEST_METHOD'] === 'GET'
? $_GET
: json_decode(file_get_contents('php://input'), true);
$event = $input['event'] ?? 'confirm';
$receipt_id = $input['receipt_id'] ?? '';
$order_id = $input['order_id'] ?? '';
if ($event === 'cancel') {
include 'views/payment/cancel.php';
exit;
}
if ($event === 'error') {
$message = '결제 중 오류가 발생했다.';
include 'views/payment/fail.php';
exit;
}
if ($event !== 'confirm' || !$receipt_id || !$order_id) {
$message = '결제 결과를 확인할 수 없다.';
include 'views/payment/fail.php';
exit;
}
$order = Order::find($order_id);
if (!$order || $order->status !== 'pending') {
$message = '승인할 수 없는 주문';
include 'views/payment/fail.php';
exit;
}
if (!checkInventory($order)) {
$message = '재고가 부족하다.';
include 'views/payment/fail.php';
exit;
}
$confirmed = BootpayApi::confirmPayment($receipt_id);
if ($confirmed['status'] !== 1 || $confirmed['price'] !== $order->price) {
$message = '결제 조회에 실패했다.';
include 'views/payment/fail.php';
exit;
}
$order->update([
'status' => 'done',
'bootpay_receipt_id' => $receipt_id,
]);
include 'views/payment/complete.php';php@RequestMapping(value = "/order/result", method = {RequestMethod.GET, RequestMethod.POST})
public String paymentResult(
@RequestParam(required = false) Map<String, String> query,
@RequestBody(required = false) Map<String, String> body,
Model model
) throws Exception {
// Web redirect는 인증 완료 후 GET query, App SDK는 POST body로 confirm을 보낸다.
Map<String, String> input = body != null ? body : query;
String event = input.getOrDefault("event", "confirm");
String receiptId = input.get("receipt_id");
String orderId = input.get("order_id");
if ("cancel".equals(event)) return "payment/cancel";
if ("error".equals(event)) {
model.addAttribute("message", "결제 중 오류가 발생했다.");
return "payment/fail";
}
if (!"confirm".equals(event) || receiptId == null || orderId == null) {
model.addAttribute("message", "결제 결과를 확인할 수 없다.");
return "payment/fail";
}
var order = orderRepository.findById(orderId);
if (order.isEmpty() || !"pending".equals(order.get().getStatus())) {
model.addAttribute("message", "승인할 수 없는 주문");
return "payment/fail";
}
if (!checkInventory(order.get())) {
model.addAttribute("message", "재고가 부족하다.");
return "payment/fail";
}
var confirmed = bootpay.confirm(receiptId);
int status = ((Number) confirmed.get("status")).intValue();
double price = ((Number) confirmed.get("price")).doubleValue();
if (status != 1 || price != order.get().getPrice()) {
model.addAttribute("message", "결제 조회에 실패했다.");
return "payment/fail";
}
order.get().setStatus("done");
order.get().setBootpayReceiptId(receiptId);
orderRepository.save(order.get());
model.addAttribute("order_id", orderId);
return "payment/complete";
}javaroute :get, :post, '/order/result' do
# Web redirect는 인증 완료 후 GET query, App SDK는 POST JSON으로 confirm을 보낸다.
data = request.get? ? params : JSON.parse(request.body.read)
event = data['event'] || 'confirm'
receipt_id = data['receipt_id']
order_id = data['order_id']
return erb :'payment/cancel' if event == 'cancel'
return erb :'payment/fail', locals: { message: '결제 중 오류가 발생했다.' } if event == 'error'
unless event == 'confirm' && receipt_id && order_id
return erb :'payment/fail', locals: { message: '결제 결과를 확인할 수 없다.' }
end
order = Order.find(order_id)
unless order && order.status == 'pending'
return erb :'payment/fail', locals: { message: '승인할 수 없는 주문' }
end
unless check_inventory(order)
return erb :'payment/fail', locals: { message: '재고가 부족하다.' }
end
confirmed = bootpay.confirm(receipt_id).data
if confirmed['status'] != 1 || confirmed['price'] != order.price
return erb :'payment/fail', locals: { message: '결제 조회에 실패했다.' }
end
order.update(status: 'done', bootpay_receipt_id: receipt_id)
erb :'payment/complete', locals: { order_id: order_id }
endrubyfunc paymentResult(w http.ResponseWriter, r *http.Request) {
// Web redirect는 인증 완료 후 GET query, App SDK는 POST JSON으로 confirm을 보낸다.
input := r.URL.Query()
if r.Method == http.MethodPost {
var body map[string]string
json.NewDecoder(r.Body).Decode(&body)
input = url.Values{}
for key, value := range body {
input.Set(key, value)
}
}
event := input.Get("event")
if event == "" { event = "confirm" }
receiptId := input.Get("receipt_id")
orderId := input.Get("order_id")
if event == "cancel" {
renderTemplate(w, "payment/cancel", nil)
return
}
if event == "error" {
renderTemplate(w, "payment/fail", map[string]string{"message": "결제 중 오류가 발생했다."})
return
}
if event != "confirm" || receiptId == "" || orderId == "" {
renderTemplate(w, "payment/fail", map[string]string{"message": "결제 결과를 확인할 수 없다."})
return
}
order, err := findOrder(orderId)
if err != nil || order.Status != "pending" {
renderTemplate(w, "payment/fail", map[string]string{"message": "승인할 수 없는 주문"})
return
}
if !checkInventory(order) {
renderTemplate(w, "payment/fail", map[string]string{"message": "재고가 부족하다."})
return
}
confirmed, err := api.ServerConfirm(receiptId)
if err != nil {
renderTemplate(w, "payment/fail", map[string]string{"message": "결제 조회에 실패했다."})
return
}
status := int(confirmed["status"].(float64))
price := confirmed["price"].(float64)
if status != 1 || price != order.Price {
renderTemplate(w, "payment/fail", map[string]string{"message": "결제 조회에 실패했다."})
return
}
markOrderDone(orderId, receiptId)
renderTemplate(w, "payment/complete", map[string]string{"order_id": orderId})
}gopublic class PaymentConfirmRequest
{
public string Event { get; set; }
[JsonPropertyName("receipt_id")] public string ReceiptId { get; set; }
[JsonPropertyName("order_id")] public string OrderId { get; set; }
}
[AcceptVerbs("GET", "POST")]
[Route("/order/result")]
public async Task<IActionResult> PaymentResult(
[FromQuery] string @event,
[FromQuery(Name = "receipt_id")] string queryReceiptId,
[FromQuery(Name = "order_id")] string queryOrderId,
[FromBody] PaymentConfirmRequest body)
{
// Web redirect는 인증 완료 후 query, App SDK는 body로 confirm을 보낸다.
var eventName = body?.Event ?? @event ?? "confirm";
var receiptId = body?.ReceiptId ?? queryReceiptId;
var orderId = body?.OrderId ?? queryOrderId;
if (eventName == "cancel") return View("Payment/Cancel");
if (eventName == "error") {
return View("Payment/Fail", new { Message = "결제 중 오류가 발생했다." });
}
if (eventName != "confirm" || string.IsNullOrEmpty(receiptId) || string.IsNullOrEmpty(orderId)) {
return View("Payment/Fail", new { Message = "결제 결과를 확인할 수 없다." });
}
var order = await orderRepository.FindAsync(orderId);
if (order == null || order.Status != "pending") {
return View("Payment/Fail", new { Message = "승인할 수 없는 주문" });
}
if (!CheckInventory(order)) {
return View("Payment/Fail", new { Message = "재고가 부족하다." });
}
var response = await bootpay.Confirm(receiptId);
var confirmed = JsonConvert.DeserializeObject<Dictionary<string, object>>(
await response.Content.ReadAsStringAsync()
);
if (Convert.ToInt32(confirmed["status"]) != 1 || Convert.ToDecimal(confirmed["price"]) != order.Price) {
return View("Payment/Fail", new { Message = "결제 조회에 실패했다." });
}
order.Status = "done";
order.BootpayReceiptId = receiptId;
await orderRepository.SaveAsync(order);
return View("Payment/Complete", new { orderId = orderId });
}csharp일부 PG사는 백엔드 승인 API를 통한 서버 승인을 지원하지 않을 수 있어요. 이때도 confirm에서 바로 승인하지 말고, 먼저 서버에 receipt_id와 order_id를 보내 주문 금액·상태·재고를 검증해야 해요. 서버에서 주문 금액·상태 확인이 통과한 경우에만 클라이언트에서 Bootpay.confirm()으로 결제 승인 요청을 진행해요.
자세한 예외 흐름과 코드는 프론트엔드 승인에서 확인해요.
5결제 결과 수신
결제 승인 요청 이후에는 성공 또는 실패 결과를 수신해요. 성공하면 승인 응답 또는 done 결과를 기준으로 서버에서 receipt_id를 다시 조회하고 주문을 확정해야 해요. 실패하면 error 또는 승인 실패 응답이 들어올 수 있으므로, 주문은 미결제 상태로 유지하고 실패 사유를 화면에 안내해요.
결제 결과 데이터 예시
프론트엔드 이벤트에서 받는 데이터는 주문 확정을 위한 전체 영수증 데이터가 아니라 다음 처리를 이어갈 식별값이에요. 핵심은 event, receipt_id, order_id이고, PG·결제수단·SDK에 따라 부가 필드가 더 붙을 수 있어요.
{
"event": "confirm",
"receipt_id": "6244f60c1fc19202e42e8c4e",
"order_id": "order_20260428_001",
"price": 1000,
"pg": "kcp",
"method": "card"
}json{
"event": "done",
"receipt_id": "6244f60c1fc19202e42e8c4e",
"order_id": "order_20260428_001",
"price": 1000,
"status": 1,
"method": "card",
"method_symbol": "card",
"purchased_at": "2026-04-28T09:30:29+09:00"
}json{
"event": "issued",
"receipt_id": "6244f60c1fc19202e42e8c4e",
"order_id": "order_20260428_001",
"method": "vbank",
"status": 5,
"vbank_data": {
"bank_name": "국민은행",
"account": "1234567890",
"account_holder": "부트페이",
"expired_at": "2026-04-29T23:59:59+09:00"
}
}json{
"event": "error",
"error_code": "RC_REQUEST_FAILED",
"message": "결제 요청이 실패했다.",
"receipt_id": "6244f60c1fc19202e42e8c4e"
}json{
"event": "cancel",
"message": "사용자가 결제를 취소했습니다.",
"receipt_id": "6244f60c1fc19202e42e8c4e"
}jsonconfirm은 결제 완료가 아니라 인증 완료/승인 직전 상태예요. done은 승인 완료 이벤트지만, 그래도 이 데이터만으로 주문을 완료하면 안 돼요. 서버에서 같은 receipt_id로 결제 조회를 호출해야 해요.
서버에서 주문 확정에 사용하는 상세 JSON은 결제 조회 응답 예시와 같은 형태예요. 결제창, 결제위젯, 웹훅 모두 최종 판단은 이 조회 응답의 status, price, order_id, receipt_id를 내부 DB 값과 비교해서 처리해요.
승인 API 호출이 실패했다면 결제 승인이 완료되지 않은 상태예요. 승인된 결제 건이 아니므로 가맹점이 별도로 취소 API를 호출할 대상도 없어요.
다만 카카오페이 같은 간편결제에서는 인증 과정에서 연결 계좌 → 카카오페이 포인트로 충전이 먼저 일어날 수 있어요. 이후 승인이 실패하거나 구매자가 중단하면 은행 계좌에서는 출금처럼 보일 수 있지만, 실제 결제 승인은 되지 않았고 금액은 카카오페이 포인트로 남아 있어요. 이 경우 구매자에게 “결제 취소”가 아니라 “간편결제 잔액 충전만 완료된 상태”라고 안내해야 해요.
가상계좌 입금 흐름
가상계좌(method: vbank)를 사용하지 않는다면 이 섹션은 건너뛰어도 돼요.
가상계좌는 결제창 종료 시점에 계좌 발급만 끝나며, 실제 결제 확정은 사용자가 입금한 뒤 PG → Bootpay → 가맹점 백엔드로 전달되는 웹훅으로 처리해야 해요.
주문 상태는 issued 시점에 WAIT_DEPOSIT으로 저장하고, 웹훅을 받은 시점에 DONE으로 전환해요. 입금이 만기 시간 안에 들어오지 않으면 PG 측에서 자동 만료되며, extra.common_event_webhook = true를 켜 두면 만료 이벤트도 함께 받을 수 있어요.
입금 완료 웹훅을 받으려면 PG사별 입금통보 URL 설정이 필요할 수 있어요. 운영 전 PG사별 가상계좌 입금통보 설정 가이드를 확인해야 해요.
6에러 처리
결제 결과 수신 단계에서는 error도 받을 수 있어요. SDK 에러는 화면 안내와 재시도 흐름으로 연결하고, 서버·PG 에러는 에러 코드표에서 원인을 확인해요.
결제 요청 시 extra.display_error_result: true를 함께 보내면, 오류가 발생했을 때 부트페이가 제공하는 결과 UI에서 에러 내용을 보여줘요. 초기 연동처럼 별도 에러 화면을 아직 만들지 않았다면 이 옵션으로 구매자에게 실패 사유를 안내할 수 있어요.
다만 UI 표시와 별개로, 주문 상태는 서버에서 receipt_id 기준으로 검증하거나 웹훅을 받아 확정해야 해요.
이 페이지는 SDK에서 자주 발생하는 에러만 다뤄요. PG사·서버 에러를 포함한 전체 코드는 에러 코드표를 참고해요.
에러 처리 코드
import { Bootpay } from '@bootpay/client-js'
try {
await Bootpay.requestPayment({
client_key: '[ Client Key ]',
price: 50000,
order_name: '나이키 운동화 외 2건',
order_id: 'order_' + Date.now(),
pg: 'nicepay',
method: 'card',
extra: {
separately_confirmed: true,
open_type: 'redirect',
redirect_url: 'https://yoursite.com/order/result',
// true면 오류 발생 시 부트페이 제공 결과 UI에 에러 내용을 표시해요.
display_error_result: true
}
})
} catch (data) {
// cancel: 사용자가 결제창을 닫은 경우. 주문은 미결제 상태로 유지해야 한다.
// error: 결제 요청·승인 과정에서 오류가 발생한 경우. error_code, pg_error_code, message를 로그로 남긴다.
if (data.event === 'cancel') {
keepOrderUnpaid(data)
return
}
if (data.event === 'error') {
logPaymentError({
error_code: data.error_code,
pg_error_code: data.pg_error_code,
message: data.message
})
showPaymentError(data.message)
}
}javascript<script src="https://js.bootpay.co.kr/bootpay-5.3.0.min.js"></script>
<script>
Bootpay.requestPayment({
client_key: '[ Client Key ]',
price: 50000,
order_name: '나이키 운동화 외 2건',
order_id: 'order_' + Date.now(),
pg: 'nicepay',
method: 'card',
extra: {
separately_confirmed: true,
open_type: 'redirect',
redirect_url: 'https://yoursite.com/order/result',
// true면 오류 발생 시 부트페이 제공 결과 UI에 에러 내용을 표시해요.
display_error_result: true
}
}).catch(function (data) {
// cancel: 사용자가 결제창을 닫은 경우. 주문은 미결제 상태로 유지해야 한다.
// error: 결제 요청·승인 과정에서 오류가 발생한 경우. error_code, pg_error_code, message를 로그로 남긴다.
if (data.event === 'cancel') {
keepOrderUnpaid(data)
return
}
if (data.event === 'error') {
logPaymentError(data.error_code, data.pg_error_code, data.message)
showPaymentError(data.message)
}
})
</script>htmlBootpay.requestPayment(viewController: self, payload: payload)
.onIssued { data in
// issued: 가상계좌 발급. 주문은 입금 대기 상태로 저장해야 한다.
self.savePendingVbankOrder(data)
}
.onConfirm { data in
// confirm: 서버 승인 방식을 쓰면 receipt_id를 백엔드로 보내야 한다.
self.requestServerApproval(data)
return false
}
.onDone { data in
// done: 서버 승인(separately_confirmed: true)에서는 호출되지 않는다.
// 클라이언트 자동 승인 방식에서만 결제 완료 후 호출된다.
}
.onCancel { data in
// cancel: 사용자가 결제창을 닫은 경우. 주문은 미결제 상태로 유지해야 한다.
self.keepOrderUnpaid(data)
}
.onError { data in
// error: 결제 요청·승인 과정에서 오류가 발생한 경우.
// error_code, pg_error_code, message를 로그로 남기고 재시도 흐름을 보여준다.
self.logPaymentError(data)
self.showPaymentError(data)
}
.onClose {
// close: 결제창이 닫힌 경우. UI 상태만 정리하고 주문 확정 처리하면 안 돼요.
self.resetPaymentUi()
}swiftBootpay.init(supportFragmentManager)
.setPayload(payload)
.setEventListener(object : BootpayEventListener {
override fun onConfirm(data: String): Boolean {
requestServerApproval(data)
return false
}
override fun onCancel(data: String) {
// 사용자가 결제창을 닫은 경우. 주문은 미결제 상태로 유지해야 한다.
keepOrderUnpaid(data)
}
override fun onError(data: String) {
// 결제 요청·승인 과정에서 오류가 발생한 경우.
// data에서 error_code, pg_error_code, message를 확인해 로그로 남긴다.
logPaymentError(data)
showPaymentError(data)
}
override fun onClose() {
// close: 결제창이 닫힌 경우. UI 상태만 정리하고 주문 확정 처리하면 안 돼요.
Bootpay.removePaymentWindow()
}
override fun onIssued(data: String) {
// issued: 가상계좌 발급. 주문은 입금 대기 상태로 저장해야 한다.
savePendingVbankOrder(data)
}
override fun onDone(data: String) {
// done: 서버 승인(separately_confirmed: true)에서는 호출되지 않는다.
// 클라이언트 자동 승인 방식에서만 결제 완료 후 호출된다.
}
})
.requestPayment()kotlinBootpay().requestPayment(
context: context,
payload: payload,
onConfirm: (data) {
// confirm: 서버 승인 방식을 쓰면 receipt_id를 백엔드로 보내야 한다.
requestServerApproval(data);
return false;
},
onIssued: (data) {
// issued: 가상계좌 발급. 주문은 입금 대기 상태로 저장해야 한다.
savePendingVbankOrder(data);
},
onDone: (data) {
// done: 서버 승인(separatelyConfirmed: true)에서는 호출되지 않는다.
// 클라이언트 자동 승인 방식에서만 결제 완료 후 호출된다.
},
onCancel: (data) {
// cancel: 사용자가 결제창을 닫은 경우. 주문은 미결제 상태로 유지해야 한다.
keepOrderUnpaid(data);
},
onError: (data) {
// error: 결제 요청·승인 과정에서 오류가 발생한 경우.
// data에서 error_code, pg_error_code, message를 확인해 로그로 남긴다.
logPaymentError(data);
showPaymentError(data);
},
onClose: () => resetPaymentUi(),
);dartBootpay().requestPayment(
context: context,
payload: payload,
onIssued: (data) {
// issued: 가상계좌 발급. 주문은 입금 대기 상태로 저장해야 한다.
savePendingVbankOrder(data);
},
onDone: (data) {
// done: 서버 승인(separatelyConfirmed: true)에서는 호출되지 않는다.
// 클라이언트 자동 승인 방식에서만 결제 완료 후 호출된다.
},
onCancel: (data) {
// cancel: Flutter Web에서 결제창이 닫히면 주문은 미결제 상태로 유지해야 한다.
keepOrderUnpaid(data);
},
onError: (data) {
// error: 결제 요청·승인 과정에서 오류가 발생한 경우.
// redirect_url로 넘어온 error 결과도 서버에서 같은 기준으로 처리해야 한다.
logPaymentError(data);
showPaymentError(data);
},
onClose: () => resetPaymentUi(),
);dart<Bootpay
ref={bootpay}
client_key={'[ Client Key ]'}
onConfirm={(data) => {
// confirm: 서버 승인 방식을 쓰면 receipt_id를 백엔드로 보내야 한다.
requestServerApproval(data)
return false
}}
onIssued={(data) => {
// issued: 가상계좌 발급. 주문은 입금 대기 상태로 저장해야 한다.
savePendingVbankOrder(data)
}}
onDone={(data) => {
// done: 서버 승인(separately_confirmed: true)에서는 호출되지 않는다.
// 클라이언트 자동 승인 방식에서만 결제 완료 후 호출된다.
}}
onCancel={(data) => {
// cancel: 사용자가 결제창을 닫은 경우. 주문은 미결제 상태로 유지해야 한다.
keepOrderUnpaid(data)
}}
onError={(data) => {
// error: 결제 요청·승인 과정에서 오류가 발생한 경우.
// error_code, pg_error_code, message를 로그로 남기고 재시도 흐름을 보여준다.
logPaymentError(data)
showPaymentError(data.message)
}}
onClose={() => {
// close: 결제창이 닫힌 경우. UI 상태만 정리하고 주문 확정 처리하면 안 돼요.
resetPaymentUi()
}}
/>jsx에러 응답 예시
{
"event": "error",
"error_code": "RC_RESOURCE_NOT_CONFIG",
"message": "결제 요청하신 결제 수단이 사용 허가가 되지 않았거나 부트페이 관리자에서 설정되지 않아 결제 진행을 할 수 없다."
}json에러 응답 필드
| 필드 | 타입 | 설명 |
|---|---|---|
| event | String | 이벤트 유형 (error, cancel) |
| error_code | String | 부트페이 에러 코드 |
| pg_error_code | String | PG사 에러 코드 |
| message | String | 에러 상세 메시지 |
에러 발생 시나리오
1결제 요청 실패
결제 요청 시 PG사가 요구하는 필수 파라미터가 누락된 경우 발생해요.
| 에러 코드 | 설명 |
|---|---|
RC_REQUEST_FAILED |
결제 요청 실패 |
RC_REQUEST_ERROR |
결제 요청 오류 |
2승인 전 오류
결제 승인 직전에 PG사 측 사유로 승인이 불가능한 경우 발생해요. 카드사 점검, PG사 내부 점검, 휴대폰 소액결제 서비스 미가입 등이 해당돼요.
| 에러 코드 | 설명 |
|---|---|
RC_CONFIRM_READY_SERVER_ERROR |
승인 준비 서버 오류 |
3승인 중 오류
결제 승인 도중 PG사 측에서 승인이 거부되는 경우예요. 한도 초과, 도난·분실 카드, 미등록 카드, 계좌이체 점검 시간, 휴대폰결제 점검 시간 등이 해당돼요.
| 에러 코드 | 설명 |
|---|---|
RC_CONFIRM_FAILED |
결제 승인 실패 |
→ 관련 문서: 결제 결과 수신 · 전체 에러 코드표
더 읽을거리
- 결제창 UX를 먼저 정하는 기준 — 결제창 방식 선택과 사용자 흐름 판단
- 간편결제 추가 여부를 결정하는 기준 — 결제수단 확장 전 확인할 운영 조건
