개별 API 사용법은 주문서 요청, 결제 검증에서 확인해요. 이 문서는 전체 흐름을 한눈에 보여줘요.
주문 구현 전에 블로그 가이드를 봐요.
- 결제만 vs 커머스 스코프 — 주문 관리를 Bootpay 에 맡길지 판단
- 정산 흐름 설계 — 3단(OrderPre/Order/OrderPurchase) 배경
- 정산 주기 / 정산 대사
- 취소·환불 정책 — 취소 승인 루프·상계
자체 주문 시스템이 있고 결제만 붙이려면 결제 흐름을 참고해요. 결제 과정은 동일하고, 이 문서는 Bootpay가 주문·이행·배송까지 관리하는 커머스 API 흐름이에요.
전체 시퀀스
참여자: 고객 | 가맹점 프론트 | 가맹점 서버 | Bootpay | PG사
| 단계 | 흐름 | 유형 | 비고 |
|---|---|---|---|
| 1 | 고객 -> 가맹점 프론트: 상품 선택 | 실선 | |
| 2 | 가맹점 프론트 -> 가맹점 서버: 주문 생성 요청 | 실선 | |
| 3 | 가맹점 서버 -> 가맹점 프론트: order_id 반환 | 점선 | |
| 4 | 가맹점 프론트 -> Bootpay: 결제위젯/체크아웃 호출 | 실선 | |
| 5 | Bootpay -> PG사: 결제 요청 | 실선 | |
| 6 | PG사 -> Bootpay: 결제 결과 | 점선 | |
| 7 | Bootpay -> 가맹점 프론트: receipt_id 반환 | 점선 | |
| 8 | 가맹점 프론트 -> 가맹점 서버: receipt_id 전달 | 실선 | |
| 9 | 가맹점 서버 -> Bootpay: 결제 검증 (GET) | 실선 | |
| 10 | Bootpay -> 가맹점 서버: 검증 결과 | 점선 | |
| 11 | 가맹점 서버: DB 저장 + 상태 변경 | 실선 | DB 저장 + 상태 변경 |
| 12 | 가맹점 서버 -> 가맹점 프론트: 결과 반환 | 점선 | |
| 13 | 가맹점 프론트 -> 고객: 완료 페이지 | 점선 | |
| 14 | Bootpay -> 가맹점 서버: 웹훅 (order.done) | 실선 | |
| 15 | 가맹점 서버: 후처리 | 실선 | 후처리 (이메일 등) |
Bootpay 내부 주문 3단계
Bootpay는 내부적으로 주문을 3단계 모델로 관리해요. API 응답과 웹훅 데이터를 이해하는 데 도움이 돼요.
흐름 OrderPre / 결제 전 임시 -> Order / 결제 확정 : 결제 완료 Order / 결제 확정 -> OrderPurchase / 이행/배송 : 판매자별 분리
노드
| ID | 라벨 | 위치 | 타입 |
|---|---|---|---|
| A | OrderPre / 결제 전 임시 | col 0, row 0 | |
| B | Order / 결제 확정 | col 1, row 0 | success |
| C | OrderPurchase / 이행/배송 | col 2, row 0 |
- OrderPre: 결제위젯이 열리기 전 생성되는 임시 주문. 이때
order_number가 발급돼요. 제한 시간(기본 30분, 몰 설정으로 변경 가능) 내에 결제하지 않으면 자동 만료돼요. - Order: 결제가 완료되면 OrderPre가 Order로 확정돼요.
order_number는 그대로 이어지고, 주문 문서 ID(order_id)는 이때 새로 생겨요. - OrderPurchase: 판매자(셀러)별 이행 단위예요. 멀티셀러 쇼핑몰이면 하나의 Order에 여러 OrderPurchase가 생길 수 있어요.
Bootpay 주문 상태코드 참조 (status)
가맹점은 아래 상태코드를 직접 관리할 필요 없지만, API 응답의 status 필드가 이 값을 따라요.
응답에는 키(문자열) 가 내려와요 — "status": "payment_completed" 처럼요.
| 값 | 키 | 가맹점 매핑 예시 | 설명 |
|---|---|---|---|
0 |
order_pending |
pending |
주문 생성됨 (결제 대기) |
1 |
payment_pending |
pending |
결제 진행 중 (무통장입금 대기 포함) |
2 |
payment_completed |
paid |
결제 완료 |
8 |
purchase_confirmed |
completed |
구매 확정 |
10 |
cancellation_completed |
refunded |
전체 취소 완료 |
11 |
return_completed |
returned |
반품 완료 |
12 |
exchange_completed |
exchanged |
교환 완료 |
-1 |
order_expired |
expired |
미결제 만료 |
전체 값은 커머스 Enum에 있어요.
단계별 상세
1단계: 주문 예비 생성 (가맹점 서버)
결제 전에 가맹점 DB에 주문을 먼저 만들어요.
// POST /api/orders — 가맹점 자체 API
app.post('/api/orders', async (req, res) => {
const { user_id, products } = req.body
// 가격은 반드시 백엔드에서 계산 (프론트엔드 금액을 믿지 않음)
const totalPrice = await calculatePrice(products)
const order = await db.orders.create({
user_id,
total_amount: totalPrice,
status: 'pending', // 아직 결제 전
items: products
})
res.json({
order_id: order.id,
price: totalPrice,
order_name: formatOrderName(products)
})
})javascript프론트엔드에서 보낸 금액을 그대로 사용하면 금액 변조 위험이 있어요. 반드시 백엔드에서 상품 가격을 조회하여 계산해요.
2단계: 결제 요청 (프론트엔드)
서버에서 받은 정보로 결제위젯 또는 체크아웃을 호출해요.
// 결제위젯 방식
Bootpay.requestPayment({
client_key: 'APPLICATION_ID',
price: orderData.price, // 서버에서 받은 금액
order_name: orderData.order_name,
order_id: String(orderData.order_id), // 가맹점 주문 ID
pg: '나이스페이',
method: '카드',
user: { id: userId, username: userName }
})
.then(response => {
// 3단계: 서버로 검증 요청
verifyPayment(response.receipt_id, orderData.order_id)
})
.catch(error => {
console.error('결제 실패:', error)
})javascript3단계: 결제 검증 (가맹점 서버)
가장 중요한 단계예요. 프론트엔드 결과만 믿으면 안 돼요.
app.post('/api/orders/verify', async (req, res) => {
const { receipt_id, order_id } = req.body
// 가맹점 DB에서 주문 조회
const order = await db.orders.findByPk(order_id)
if (!order) return res.status(404).json({ error: '주문을 찾을 수 없음' })
// Bootpay에서 결제 정보 조회
const receipt = await Bootpay.receiptPayment(receipt_id)
// 검증: 상태 + 금액
if (receipt.status === 1 && receipt.price === order.total_amount) {
// ✅ 검증 성공 → DB 업데이트
await db.orders.update(
{
status: 'paid',
bootpay_receipt_id: receipt_id,
paid_at: new Date()
},
{ where: { id: order_id } }
)
res.json({ success: true })
} else {
// ❌ 검증 실패 → 결제 취소
await Bootpay.cancelPayment({
receipt_id,
cancel_price: receipt.price,
cancel_reason: '금액 위변조 의심'
})
res.json({ success: false, message: '검증 실패' })
}
})javascript4단계: 웹훅 수신 (비동기 후처리)
결제 검증과 별개로, Bootpay가 웹훅을 발송해요. 후처리 로직에 활용해요.
// 웹훅 수신 후 처리 (상세: /webhook/guide)
app.post('/webhooks/bootpay', async (req, res) => {
res.status(200).json({ success: true }) // 즉시 200 반환
const { webhook_type, payload: data } = req.body
if (webhook_type === 'order.done') {
// 이메일 발송, 재고 차감, 포인트 적립 등
await sendOrderConfirmEmail(data.order_number)
await decrementStock(data.order_number)
}
})javascript주문 상태 관리
가맹점 DB의 주문 상태는 아래 흐름을 따른다:
상태
| ID | 라벨 | 위치 | 타입 |
|---|---|---|---|
| start | col 0, row 1 | initial | |
| pending | pending | col 1, row 1 | |
| paid | paid | col 2, row 1 | active |
| confirmed | confirmed | col 3, row 1 | |
| completed | completed | col 4, row 1 | active |
| refunded | refunded | col 3, row 2 | end |
| expired | expired | col 1, row 2 | end |
전이 -> pending pending -> paid paid -> confirmed confirmed -> completed paid -> refunded : 환불 pending -> expired : 결제 안 함
아래는 가맹점 DB 의 orders.status 예시예요 — Bootpay 응답의 status 와는 다른 값 체계입니다.
가맹점 orders.status |
설명 | 변경 시점 |
|---|---|---|
pending |
주문 생성됨, 결제 대기 | 주문 예비 생성 시 |
paid |
결제 완료, 검증 성공 | 서버 검증 성공 후 |
confirmed |
주문 확인 (출고 준비) | 가맹점이 수동 또는 자동으로 |
completed |
배송/서비스 완료 | 배송 완료 확인 후 |
refunded |
환불 완료 | 웹훅 order.cancelled 수신 시 |
expired |
미결제로 만료 | 일정 시간 후 자동 (정책에 따라) |
디지털 상품이면 paid → completed를 바로 처리해도 돼요. 물리적 배송이 있으면 confirmed(출고) 단계를 거쳐요.
취소/환불 흐름
주문 취소는 전체 취소와 부분 취소(금액 또는 상품 단위)로 나뉘어요.
전체 취소 vs 부분 취소
| 유형 | 설명 | 사용 시점 |
|---|---|---|
| 전체 취소 | 주문 전액 환불 | 주문 전체를 취소할 때 |
| 부분 취소 (금액) | 지정 금액만 환불 | 쿠폰 할인 차감 등 금액 조정 |
| 부분 취소 (상품) | 특정 상품만 취소 | 여러 상품 중 일부만 환불 |
취소 요청 → 승인 → 완료 흐름
참여자: 가맹점 서버 | Bootpay
| 단계 | 흐름 | 유형 | 비고 |
|---|---|---|---|
| 1 | 가맹점 서버 -> Bootpay: 취소 요청 (POST) | 실선 | |
| 2 | Bootpay: 취소 금액 계산 · 자동승인 확인 | 실선 | 취소 금액 계산 · 자동승인 확인 |
| 3 | Bootpay -> 가맹점 서버: 승인 결과 | 점선 | |
| 4 | Bootpay: PG사 환불 · 상태 변경 | 실선 | PG사 환불 · 상태 변경 |
| 5 | Bootpay -> 가맹점 서버: 웹훅 (order.cancelled) | 실선 | |
| 6 | 가맹점 서버: 후처리: 재고 복구 · 고객 안내 | 실선 | 재고 복구 · 서비스 비활성화 · 안내 이메일 |
취소 완료 웹훅 처리
// 웹훅 수신: order.cancelled (전체 취소) · order.partial_cancelled (부분 취소)
async function handleOrderCancelled(webhookType, data) {
const { order_number, price, cancelled_price } = data
// 1. 주문 상태 업데이트
if (webhookType === 'order.cancelled') {
// 전체 취소
await db.orders.update(
{ status: 'refunded' },
{ where: { bootpay_order_number: order_number } }
)
} else {
// 부분 취소 — 잔액(price - cancelled_price)이 남아 있으면 주문은 유지
await db.orders.update(
{ total_amount: price - cancelled_price, status: 'partially_refunded' },
{ where: { bootpay_order_number: order_number } }
)
}
// 2. 재고 복구 (전체 취소 시)
// 3. 환불 안내 이메일
}javascriptorder.partial_cancelled 는 지금 payload 없이 와요. 받으면 주문 상세를 다시 조회해 price·cancelled_price 를 읽어요.
부분 취소가 발생하면 주문 자체는 유효해요. status를 refunded로 바꾸면 안 되고, 잔여 금액을 기준으로 서비스를 유지해야 해요.
이행(Fulfillment) 흐름
물리적 배송이 있는 커머스에서는 결제 완료 후 이행 단계를 관리해야 해요.
이행 상태 흐름
흐름 ready / (발주대기) -> confirmed / (발주확인) confirmed / (발주확인) -> shipped / (배송중) shipped / (배송중) -> delivered / (배송완료)
노드
| ID | 라벨 | 위치 | 타입 |
|---|---|---|---|
| A | ready / (발주대기) | col 0, row 0 | |
| B | confirmed / (발주확인) | col 1, row 0 | |
| C | shipped / (배송중) | col 2, row 0 | process |
| D | delivered / (배송완료) | col 3, row 0 | success |
| 가맹점 이행 상태 | Bootpay order_purchases[].status |
가맹점 액션 |
|---|---|---|
ready |
0(주문 전) ~ 1(발주 대기) |
발주 접수. 상품 준비 시작 |
confirmed |
2 (발주 확인) |
발주 확인 완료 |
preparing |
3 (상품 준비중) |
상품 준비 |
shipped |
4 (발송 완료) |
배송 시작. 운송장 번호 등록 |
delivered |
5 (배송 완료) |
배송 완료. 구매 확정 대기 |
주문 status 와 달리 발주의 order_purchases[].status 는 enum 문자열로 변환되지 않고 숫자 그대로 내려와요.
같은 배열의 progress_status 는 발송처리로 갱신되지 않으니 이행 판단에 쓰지 않아요. 가맹점 스키마의 progress_status(위 표 왼쪽 열)와도 값 체계가 달라요.
배송 정보 연동 패턴
배송은 발주 단위로 움직여요. 운송장을 발송처리로 넣으면 그때부터 배송 추적이
자동으로 붙고, 택배사가 알려주는 단계는 주문 상세의 order_purchases[]로 읽어요.
// 발송처리 (가맹점 서버에서)
async function shipOrder(orderNumber, purchaseNumber, trackingInfo) {
await fetch(`https://api.bootapi.com/v1/orders/${ orderNumber }/purchases`, {
method: 'PUT',
headers: { Authorization: basicAuth, 'Content-Type': 'application/json' },
body: JSON.stringify({
purchases: [
{
order_purchase_number: purchaseNumber,
status: 4, // 발송 완료
delivery_company_code: trackingInfo.company_code, // 택배사 코드
tracking_number: trackingInfo.tracking_number // 운송장 번호
}
]
})
})
}
// 배송 상태 동기화 (가맹점 서버에서)
async function syncShipping(orderNumber) {
const order = await commerce.order.detail(orderNumber)
for (const purchase of order.order_purchases) {
await db.order_fulfillments.update(
{
progress_status: purchase.status, // 4 발송 완료 · 5 배송 완료
delivery_company: purchase.delivery_company_code, // 택배사 코드
tracking_number: purchase.tracking_number, // 운송장 번호
tracking_status: purchase.d_ts // 배송추적 상태
},
{ where: { bootpay_order_purchase_number: purchase.order_purchase_number } }
)
}
}javascripttracking_histories[]에는 집화·배송 완료 같은 추적 단계가 최신 순으로 쌓여요. 값은
커머스 Enum의 발주 상태·배송추적 상태 표에서 확인하세요.
택배사가 배송 완료를 알려주면 발주 상태도 따라 넘어가요. 발송처리까지만 해두면 나머지는 자동이에요.
배송이 없는 디지털 상품이라면 이행 단계를 건너뛰고, 결제 완료 시 즉시 서비스를 활성화하면 돼요.
흔한 실수
1검증 없이 프론트엔드 결과만 믿기
// ❌ 절대 하면 안 됨
Bootpay.requestPayment({ ... })
.then(response => {
// 프론트엔드 결과만으로 주문 완료 처리
db.orders.update({ status: 'paid' }) // 위험!
})javascript프론트엔드 응답은 위변조가 가능해요. 반드시 백엔드에서 결제 검증을 수행해요.
2웹훅에만 의존하기
// ❌ 위험 — 웹훅이 지연되면 고객이 결제했는데 서비스를 못 씀
// 웹훅만으로 주문 완료 처리javascript동기 검증(3단계)이 메인 흐름이고, 웹훅은 보조 수단(이메일, 재고 등 후처리)예요.
3가격을 프론트엔드에서 계산
// ❌ 위험 — 프론트엔드에서 할인 적용
const price = product.price * 0.8 // 20% 할인
Bootpay.requestPayment({ price }) // 변조 가능javascript가격 계산은 반드시 서버에서 수행하고, 결제 검증 시 서버의 가격과 비교해요.
