상품 판매 페이지에 결제 기능을 추가해요.
이럴 때 필요해요: 상품 판매 페이지에 결제를 붙일 때, 구독 요금제 주문서를 만들 때
내가 할 일: 관리자(상품 등록) + 프론트엔드(주문서 SDK) + 백엔드(주문 검증·웹훅)
Commerce SDK를 사용하여 구독/요금제 결제를 구현해요. 주문서를 통해 상품 선택부터 결제까지 한 번에 처리할 수 있어요.
빠르게 시작하려면?
체크아웃 빠른 매뉴얼 — JavaScript 최소 코드로 5분 안에 연동해요. 이 페이지는 전체 파라미터와 할인/프로모션을 다루는 Full Reference예요.
내가 하는 것
- 관리자 상품 관리에서 상품 등록 (또는 API로 등록)
- 서버에서 주문 완료 후 검증
- 웹훅으로 주문 상태 동기화
Bootpay가 알아서 하는 것
- 주문서 UI 호스팅, 결제 처리
- 주문 생성·저장, 고객 데이터 관리
- 웹훅 알림, 취소/환불 처리
연동 흐름
상품 등록 (관리자)
Bootpay 관리자 상품 대시보드에서 상품을 등록해요. 상품명, 가격, 구독 설정 등을 입력하면 product_id가 발급돼요.
::: tip API로 상품을 등록할 수도 있어요. 상품 등록 가이드를 참고해요.
주문서 요청 (개발자 · 프론트엔드)
import { BootpayCommerce } from '@bootpay/commerce-js'
const CLIENT_KEY = 'your-client-key'
async function selectPlan(planKey) {
// 서버에 주문 생성 요청 (위변조 방지)
const orderRes = await fetch('/api/orders/plan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ plan_key: planKey })
})
const orderData = await orderRes.json()
// Commerce 주문서 요청
const response = await BootpayCommerce.requestCheckout({
client_key: CLIENT_KEY,
name: orderData.order_name,
price: orderData.price,
redirect_url: window.location.origin + '/result',
user: {
membership_type: 'guest',
user_id: 'user_1234',
name: '홍길동',
phone: '01012345678',
email: 'user@example.com'
},
products: [
{
product_id: 'product_id_here',
duration: -1,
quantity: 1
}
],
metadata: {
order_id: orderData.order_id,
plan_key: planKey
},
extra: {
separately_confirmed: false,
create_order_immediately: true
}
})
}javascript<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>요금제 선택</title>
<script src="https://js.bootpay.co.kr/bp-commerce-sdk-latest.min.js"></script>
</head>
<body>
<script>
const CLIENT_KEY = 'your-client-key'
async function selectPlan(planKey) {
try {
// 서버에 주문 생성 요청 (위변조 방지)
const orderRes = await fetch('/api/orders/plan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ plan_key: planKey })
})
const orderData = await orderRes.json()
// Commerce 주문서 요청
const response = await BootpayCommerce.requestCheckout({
client_key: CLIENT_KEY,
name: orderData.order_name,
price: orderData.price,
redirect_url: window.location.origin + '/result.html',
user: {
membership_type: 'guest',
user_id: 'user_1234',
name: '홍길동',
phone: '01012345678',
email: 'user@example.com'
},
products: [
{
product_id: 'product_id_here',
duration: -1, // 무기한 구독
quantity: 1
}
],
metadata: {
order_id: orderData.order_id,
plan_key: planKey
},
extra: {
separately_confirmed: false,
create_order_immediately: true
}
})
} catch (error) {
console.error('주문서 요청 오류:', error)
}
}
</script>
</body>
</html>html// web/index.html <head>에 Commerce JS SDK 추가 필수:
// <script src="https://js.bootpay.co.kr/bp-commerce-sdk-latest.min.js"></script>
import 'dart:js_interop';
@JS('BootpayCommerce.requestCheckout')
external JSPromise<JSAny> _requestCheckout(JSAny options);
Future<void> selectPlan(String planKey) async {
// 서버에 주문 생성 요청 (위변조 방지)
final orderData = await api.createOrder(planKey);
final options = {
'client_key': 'your-client-key',
'name': orderData['order_name'],
'price': orderData['price'],
'redirect_url': '${Uri.base.origin}/result',
'user': {
'membership_type': 'guest',
'user_id': 'user_1234',
'name': '홍길동',
'phone': '01012345678',
'email': 'user@example.com',
},
'products': [
{
'product_id': 'product_id_here',
'duration': -1,
'quantity': 1,
}
],
'metadata': {
'order_id': orderData['order_id'],
'plan_key': planKey,
},
'extra': {
'separately_confirmed': false,
'create_order_immediately': true,
},
}.jsify();
await _requestCheckout(options).toDart;
}dart모바일 앱 (Android · iOS · Flutter · React Native)
Commerce 주문서는 웹 리디렉트 방식이에요. 모바일 앱에서는 WebView로 주문서 페이지를 열거나, 결제 요청에서 일반 결제 SDK를 사용해요.
요청 파라미터
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
client_key |
String | 필수 | Commerce 클라이언트 키 |
name |
String | 필수 | 주문명 (예: "Professional 플랜") |
price |
Integer | 필수 | 서버에서 계산한 결제 금액 |
redirect_url |
String | 필수 | 결제 완료 후 이동 URL |
products |
Array | 필수 | 구독 상품 배열 |
products[].product_id |
String | 필수 | 상품 ID |
products[].duration |
Integer | 필수 | 구독 기간 (-1: 무기한) |
products[].quantity |
Integer | 필수 | 수량 |
user |
Object | 필수 | 구매자 정보 |
user.membership_type |
String | 필수 | 회원 유형 (guest/member) |
user.name |
String | 필수 | 구매자 이름 |
user.phone |
String | 선택 | 구매자 전화번호 |
user.email |
String | 선택 | 구매자 이메일 |
request_id |
String | 선택 | 청구서 ID (할인 적용 시) |
metadata |
Object | 선택 | 가맹점 커스텀 데이터 |
extra |
Object | 선택 | 추가 옵션 |
extra.separately_confirmed |
Boolean | 선택 | 분리 승인 모드 |
extra.create_order_immediately |
Boolean | 선택 | 즉시 주문 생성 여부 |
결제 완료 처리 (개발자 · 프론트엔드)
결제 완료 후 redirect_url로 이동하며, URL 파라미터로 결과를 전달해요.
| 파라미터 | 설명 |
|---|---|
order_number |
Commerce 주문 고유 번호 |
event |
결과 이벤트 (done/cancel/error) |
프론트엔드 결과만으로 주문을 완료하면 안 돼요. 반드시 서버에서 주문 검증을 수행해요.
서버 검증 (개발자 · 서버)
GET
https://api.bootapi.com/v1/orders/{order_number}Basic Authconst express = require('express')
const { Bootpay } = require('@bootpay/backend-js')
const { BootpayCommerce } = require('@bootpay/backend-js')
const commerce = new BootpayCommerce({
client_key: 'your-commerce-client-key',
secret_key: 'your-commerce-secret-key',
mode: 'production'
})
// 결제 검증 API
app.post('/api/orders/verify', async (req, res) => {
const { receipt_id, order_id } = req.body
// 주문 정보 조회
const order = await getOrder(order_id) // DB에서 조회
// Bootpay 결제 정보 조회
Bootpay.setConfiguration({
client_key: 'REST_APPLICATION_ID',
secret_key: 'PRIVATE_KEY'
})
const receipt = await Bootpay.getReceipt(receipt_id)
// 금액 검증 (위변조 방지)
if (receipt.status === 1 && receipt.price === order.price) {
// 주문 완료 처리
res.json({ success: true })
} else {
// 금액 불일치 — 결제 취소
await Bootpay.cancelPayment({
receipt_id,
cancel_price: receipt.price,
cancel_reason: '금액 위변조 의심'
})
res.json({ success: false, message: '금액 불일치' })
}
})javascriptfrom flask import Flask, request, jsonify
from bootpay_backend import Bootpay, BootpayCommerce
commerce = BootpayCommerce(
client_key='your-commerce-client-key',
secret_key='your-commerce-secret-key',
mode='production'
)
@app.route('/api/orders/verify', methods=['POST'])
def verify_payment():
data = request.get_json()
receipt_id = data['receipt_id']
order_id = data['order_id']
# 주문 정보 조회
order = get_order(order_id) # DB에서 조회
# Bootpay 결제 정보 조회
bootpay = Bootpay('APPLICATION_ID', 'PRIVATE_KEY')
receipt = bootpay.get_receipt(receipt_id)
# 금액 검증
if receipt['status'] == 1 and receipt['price'] == order['price']:
return jsonify({'success': True})
else:
bootpay.cancel_payment(
receipt_id=receipt_id,
cancel_price=receipt['price'],
cancel_reason='금액 위변조 의심'
)
return jsonify({'success': False, 'message': '금액 불일치'})pythonuse Bootpay\ServerPhp\BootpayApi;
$bootpay = new BootpayApi('APPLICATION_ID', 'PRIVATE_KEY');
$data = json_decode(file_get_contents('php://input'), true);
$receipt_id = $data['receipt_id'];
$order_id = $data['order_id'];
// 주문 정보 조회
$order = getOrder($order_id); // DB에서 조회
// Bootpay 결제 정보 조회
$receipt = $bootpay->receiptPayment($receipt_id);
// 금액 검증
if ($receipt['status'] === 1 && $receipt['price'] === $order['price']) {
echo json_encode(['success' => true]);
} else {
$bootpay->cancelPayment([
'receipt_id' => $receipt_id,
'cancel_price' => $receipt['price'],
'cancel_reason' => '금액 위변조 의심'
]);
echo json_encode(['success' => false, 'message' => '금액 불일치']);
}phpimport kr.co.bootpay.Bootpay;
Bootpay bootpay = new Bootpay("APPLICATION_ID", "PRIVATE_KEY");
// 주문 정보 조회
var order = getOrder(orderId); // DB에서 조회
// Bootpay 결제 정보 조회
var receipt = bootpay.receiptPayment(receiptId);
// 금액 검증
if (receipt.getStatus() == 1 && receipt.getPrice() == order.getPrice()) {
return Map.of("success", true);
} else {
bootpay.cancelPayment(Map.of(
"receipt_id", receiptId,
"cancel_price", receipt.getPrice(),
"cancel_reason", "금액 위변조 의심"
));
return Map.of("success", false, "message", "금액 불일치");
}javabootpay = Bootpay::Api.new('APPLICATION_ID', 'PRIVATE_KEY')
# 주문 정보 조회
order = get_order(order_id) # DB에서 조회
# Bootpay 결제 정보 조회
receipt = bootpay.receipt_payment(receipt_id)
# 금액 검증
if receipt['status'] == 1 && receipt['price'] == order['price']
{ success: true }.to_json
else
bootpay.cancel_payment(
receipt_id: receipt_id,
cancel_price: receipt['price'],
cancel_reason: '금액 위변조 의심'
)
{ success: false, message: '금액 불일치' }.to_json
endrubyimport "github.com/bootpay/backend-go/v2"
api := bootpay.NewApi("APPLICATION_ID", "PRIVATE_KEY")
// 주문 정보 조회
order := getOrder(orderId) // DB에서 조회
// Bootpay 결제 정보 조회
receipt, err := api.ReceiptPayment(receiptId)
if err != nil {
log.Fatal(err)
}
// 금액 검증
if receipt.Status == 1 && receipt.Price == order.Price {
json.NewEncoder(w).Encode(map[string]bool{"success": true})
} else {
api.CancelPayment(bootpay.CancelParams{
ReceiptId: receiptId,
CancelPrice: receipt.Price,
CancelReason: "금액 위변조 의심",
})
json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": "금액 불일치"})
}gousing Bootpay;
var bootpay = new BootpayApi("APPLICATION_ID", "PRIVATE_KEY");
// 주문 정보 조회
var order = await GetOrder(orderId); // DB에서 조회
// Bootpay 결제 정보 조회
var receipt = await bootpay.ReceiptPayment(receiptId);
// 금액 검증
if (receipt.Status == 1 && receipt.Price == order.Price)
{
return Ok(new { success = true });
}
else
{
await bootpay.CancelPayment(new {
receipt_id = receiptId,
cancel_price = receipt.Price,
cancel_reason = "금액 위변조 의심"
});
return Ok(new { success = false, message = "금액 불일치" });
}csharp:::
응답
주문 조회 응답
{
"order_id": "68707c59b0eacea5cd974efd",
"order_name": "구독상품 - 1회차",
"price": 2800,
"status": 2,
"receipt_status": 1,
"order_number": "25071182085082524116",
"purchased_at": "2025-07-11T02:52:09Z",
"created_at": "2025-07-11T02:52:09Z",
"result_data": {
"receipt_id": "68707c598bbe23d076af189c",
"pg": "라이트페이",
"method": "카드",
"status": 1,
"card_data": {
"card_company": "KB국민",
"card_no": "557042******1074"
}
},
"order_subscriptions": {
"subscriptions": [
{
"order_subscription_id": "68707c58b0eacea5cd974ef7",
"status": 1,
"current_duration": 3,
"total_subscription_duration": 4
}
]
}
}json:::
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
order_id |
String | 필수 | Commerce 주문 ID |
order_name |
String | 필수 | 주문명 |
price |
Integer | 필수 | 결제 금액 |
status |
Integer | 필수 | 주문 상태 (0: 대기, 1: 결제완료, 2: 확인) |
receipt_status |
Integer | 필수 | 결제 영수증 상태 (1: 완료) |
order_number |
String | 필수 | 주문 고유번호 |
purchased_at |
String | 필수 | 결제 일시 (ISO 8601) |
created_at |
String | 필수 | 주문 생성 일시 |
result_data |
Object | 선택 | 결제 결과 상세 |
receipt_id |
String | 필수 | Bootpay 영수증 ID |
pg |
String | 필수 | PG사 이름 |
method |
String | 필수 | 결제 수단 |
status |
Integer | 필수 | 결제 상태 (1: 완료) |
card_data |
Object | 선택 | 카드 결제 시 카드 정보 |
card_company |
String | 필수 | 카드사 |
card_no |
String | 필수 | 마스킹된 카드번호 |
order_subscriptions |
Object | 선택 | 구독 정보 |
subscriptions |
Array | 필수 | 구독 목록 |
order_subscription_id |
String | 필수 | 구독 ID |
status |
Integer | 필수 | 구독 상태 (1: 활성) |
current_duration |
Integer | 필수 | 현재 구독 회차 |
total_subscription_duration |
Integer | 필수 | 총 구독 회차 |
주문 상태 관리
주문서 결제 후 가맹점 DB에서 주문 상태를 관리해요.
| 시점 | 가맹점 DB 상태 | 비고 |
|---|---|---|
| 주문서 요청 전 | pending |
주문 예비 생성 (서버에서 가격 계산) |
| 서버 검증 성공 | paid |
bootpay_order_id, bootpay_order_number 저장 |
| 배송 시작 | shipping |
가맹점이 직접 변경 |
| 배송 완료 | completed |
가맹점이 직접 변경 |
| 환불 웹훅 수신 | refunded |
order.cancelled 웹훅으로 확인 |
에러 코드
공통 에러
인증·권한 관련 에러는 에러 코드표를 참고해요.
| 코드 | 메시지 | 대처 방법 |
|---|---|---|
ORDER_PRODUCT_BLANK |
주문한 상품 정보가 없어요. | products 파라미터를 확인해요 |
ORDER_PRODUCT_NOT_FOUND |
상품 정보가 없어요 | product_id를 확인해요 |
ORDER_PRICE_NOT_MATCH |
요청된 금액과 상품의 결제 금액이 일치하지 않는다 | price 계산을 확인해요 |
ORDER_ADDRESS_BLANK |
배송상품의 경우 배송지 정보가 필수예요. 배송지 정보를 입력해요. | address 정보를 입력해요 |
할인/프로모션 적용
서버에서 청구서를 미리 생성하여 할인을 적용할 수 있어요.
// 서버에서 청구서 생성 (할인 적용)
const invoice = await commerce.invoice.create({
products: [{ product_id: 'product_123', quantity: 1 }],
price: finalPrice, // 할인 적용된 최종 금액
original_price: originalPrice,
user: { user_id: 'user_1234', name: '홍길동' },
metadata: { promotion_code: 'SUMMER2024' }
})
// 프론트엔드에서 invoice_id를 request_id로 전달
BootpayCommerce.requestCheckout({
request_id: invoice.data.invoice_id,
// ...
})javascript
