결제 조회는 프론트엔드에서 받은 receipt_id로 Bootpay 결제 내역을 서버에서 다시 조회하고, 내부 주문 정보와 비교하는 단계예요.
프론트엔드의 성공 콜백만으로 주문을 완료 처리하면 안 돼요. 브라우저에서 받은 값은 누락되거나 조작될 수 있으므로, 서버가 Bootpay API로 결제 상태와 금액을 다시 확인해야 해요.
이 문서는 서버에서 receipt_id로 결제 내역을 조회하고 내부 주문 정보와 비교하는 방법을 설명해요.
주문 상태를 done으로 바꾸기 전, 또는 웹훅으로 받은 이벤트를 처리하기 전에 같은 조회 흐름을 사용해요.
핵심 요약
- 결제 조회 API는
receipt_id로 결제 내역을 조회해요. - 주문 완료 조건은 보통 상태가 결제 완료이고, 결제 금액이 내부 주문 금액과 일치하는 것이에요.
receipt_id, 결제 금액, 결제수단, 결제 완료 시각은 주문 DB에 저장해 둬요.- 웹훅을 받았을 때도 웹훅 데이터만 믿지 말고 같은 방식으로 다시 조회해요.
- 금액이나 상태가 맞지 않으면 주문을 완료 처리하면 안 돼요.
API 엔드포인트
https://api.bootpay.co.kr/v2/receipt/{receipt_id}Basic Auth| 파라미터 | 위치 | 필수 | 설명 |
|---|---|---|---|
receipt_id |
Path | 필수 | 결제 완료, 승인 대기, 웹훅 이벤트에서 받은 Bootpay 영수증 ID |
lookup_user_data |
Query | 선택 | true면 응답에 구매자 user 객체(결제창에서 수집된 username, phone, email 등)를 포함해요. 수집되지 않은 건은 user가 null이에요 |
서버 SDK를 사용하면 인증 헤더와 요청 처리를 SDK가 대신 처리해요. 직접 API를 호출할 때는 API 인증을 먼저 확인해요.
언제 조회하나
결제 조회는 서버가 결제 결과를 처음 인지하는 시점마다 수행해요.
| 상황 | 서버가 할 일 |
|---|---|
프론트엔드 onDone 수신 |
클라이언트 자동 승인 흐름에서 receipt_id를 받아 결제 조회 후 주문 완료 처리 |
프론트엔드 onConfirm 수신 |
서버 승인 흐름에서 승인 전 금액과 주문 정보를 확인한 뒤 승인 처리. 이 경우 onDone은 호출되지 않음 |
| 웹훅 수신 | 웹훅의 receipt_id로 다시 조회한 뒤 주문 상태 보정 |
| 취소·환불 처리 전 | 저장된 주문 상태와 결제 상태를 확인한 뒤 취소 API 호출 |
웹훅 데이터도 그대로 믿으면 안 돼요.
웹훅은 상태 변경을 알려주는 신호이고, 최종 처리는 receipt_id 조회 결과와 내부 주문 정보를 비교한 뒤 진행해요.
기본 조회 흐름
| 순서 | 위치 | 내용 |
|---|---|---|
| 1 | 프론트엔드 | 클라이언트 자동 승인 흐름에서는 done, 서버 승인 흐름에서는 confirm으로 receipt_id를 받는다 |
| 2 | 프론트엔드 → 서버 | receipt_id와 내부 order_id를 서버로 보낸다 |
| 3 | 서버 | 내부 주문을 조회해 예상 금액과 상태를 확인해요 |
| 4 | 서버 → Bootpay | GET /v2/receipt/{receipt_id}를 호출한다 |
| 5 | 서버 | 응답의 금액, 상태, 주문번호를 내부 주문과 비교한다 |
| 6 | 서버 | 조회 결과가 내부 주문과 일치하면 주문 상태를 done으로 저장한다 |
| 7 | 서버 | 실패 시 주문을 완료 처리하지 않고 실패 사유를 기록한다 |
조회 후 확인해야 할 값
최소한 아래 값은 비교해요.
| 값 | 확인 기준 |
|---|---|
status |
일반 결제 완료는 1인지 확인해요 |
price |
내부 주문 금액과 일치하는지 확인해요. price는 원 결제금액이 아니라 취소분을 뺀 잔액이라, 취소가 있었다면 원 주문금액보다 작아요 (전액취소 시 0) |
order_id |
내부 주문 ID와 같은 결제 건인지 확인해요 |
receipt_id |
이미 처리한 영수증인지 확인해 중복 처리를 막는다 |
currency |
서비스가 기대한 통화인지 확인해요 |
method_symbol |
허용한 결제수단인지 확인해요 |
분리 승인 흐름에서는 승인 전 상태를 확인해야 하므로 일반 결제 완료와 기준이 다를 수 있어요. 서버 승인 방식은 분리 승인과 결제 흐름 설계를 함께 확인해요.
코드 예제
아래 예제의 핵심은 SDK 호출이 아니라 내부 주문과 Bootpay 조회 결과를 비교한 뒤 주문 상태를 바꾸는 것이에요.
import { Bootpay } from '@bootpay/backend-js'
Bootpay.setConfiguration({
client_key: process.env.BOOTPAY_CLIENT_KEY,
secret_key: process.env.BOOTPAY_SECRET_KEY,
})
export async function verifyPayment({ orderId, receiptId }) {
const order = await db.orders.findByPk(orderId)
if (!order) throw new Error('주문을 찾을 수 없습니다')
if (order.status === 'done') return order
const receipt = await Bootpay.receiptPayment(receiptId)
if (receipt.status !== 1) {
throw new Error('결제 완료 상태가 아닙니다')
}
if (receipt.price !== order.amount) {
throw new Error('결제 금액이 주문 금액과 다릅니다')
}
if (receipt.order_id && receipt.order_id !== order.order_id) {
throw new Error('주문번호가 일치하지 않습니다')
}
await db.orders.update({
bootpay_receipt_id: receipt.receipt_id,
paid_amount: receipt.price,
payment_method: receipt.method_symbol,
payment_status: receipt.status,
status: 'done',
paid_at: receipt.purchased_at ? new Date(receipt.purchased_at) : new Date(),
}, { where: { id: order.id } })
return db.orders.findByPk(order.id)
}javascriptimport os
from bootpay_backend import BootpayBackend
bootpay = BootpayBackend(
client_key=os.environ['BOOTPAY_CLIENT_KEY'],
secret_key=os.environ['BOOTPAY_SECRET_KEY'],
)
def verify_payment(order_id, receipt_id):
order = Order.get(order_id)
if order.status == 'done':
return order
receipt = bootpay.receipt_payment(receipt_id)
if receipt['status'] != 1:
raise Exception('결제 완료 상태가 아닙니다')
if receipt['price'] != order.amount:
raise Exception('결제 금액이 주문 금액과 다릅니다')
order.bootpay_receipt_id = receipt['receipt_id']
order.paid_amount = receipt['price']
order.payment_method = receipt.get('method_symbol')
order.status = 'done'
order.paid_at = receipt.get('purchased_at')
order.save()
return orderpython$receipt = BootpayApi::receiptPayment($receiptId);
$order = Order::find($orderId);
if (!$order) {
throw new Exception('주문을 찾을 수 없습니다');
}
if ($order->status === 'done') {
return $order;
}
if ($receipt['status'] !== 1) {
throw new Exception('결제 완료 상태가 아닙니다');
}
if ($receipt['price'] !== $order->amount) {
throw new Exception('결제 금액이 주문 금액과 다릅니다');
}
$order->update([
'bootpay_receipt_id' => $receipt['receipt_id'],
'paid_amount' => $receipt['price'],
'payment_method' => $receipt['method_symbol'] ?? null,
'payment_status' => $receipt['status'],
'status' => 'done',
'paid_at' => $receipt['purchased_at'] ?? date('c'),
]);phpvar order = orderRepository.findById(orderId)
.orElseThrow(() -> new IllegalArgumentException("주문을 찾을 수 없습니다"));
if ("done".equals(order.getStatus())) {
return order;
}
var receipt = bootpay.getReceipt(receiptId);
int status = ((Number) receipt.get("status")).intValue();
double price = ((Number) receipt.get("price")).doubleValue();
if (status != 1) {
throw new IllegalStateException("결제 완료 상태가 아닙니다");
}
if (price != order.getAmount()) {
throw new IllegalStateException("결제 금액이 주문 금액과 다릅니다");
}
order.setBootpayReceiptId((String) receipt.get("receipt_id"));
order.setPaidAmount(price);
order.setPaymentStatus(status);
order.setStatus("done");
orderRepository.save(order);javaorder = Order.find(order_id)
return order if order.status == 'done'
receipt = bootpay.verify(receipt_id).data
raise '결제 완료 상태가 아닙니다' unless receipt['status'] == 1
raise '결제 금액이 주문 금액과 다릅니다' unless receipt['price'] == order.amount
order.update(
bootpay_receipt_id: receipt['receipt_id'],
paid_amount: receipt['price'],
payment_method: receipt['method_symbol'],
payment_status: receipt['status'],
status: 'done',
paid_at: receipt['purchased_at'] || Time.now
)rubyorder, err := findOrder(orderID)
if err != nil {
return err
}
if order.Status == "done" {
return nil
}
receipt, err := api.GetReceipt(receiptID)
if err != nil {
return err
}
status := int(receipt["status"].(float64))
price := receipt["price"].(float64)
bootpayReceiptID := receipt["receipt_id"].(string)
if status != 1 {
return errors.New("결제 완료 상태가 아닙니다")
}
if price != order.Amount {
return errors.New("결제 금액이 주문 금액과 다릅니다")
}
return markOrderPaid(orderID, bootpayReceiptID, price)govar order = await orderRepository.FindAsync(orderId);
if (order == null) throw new Exception("주문을 찾을 수 없습니다");
if (order.Status == "done") return order;
var response = await bootpay.GetReceipt(receiptId);
var body = await response.Content.ReadAsStringAsync();
var receipt = JsonConvert.DeserializeObject<Dictionary<string, object>>(body);
var status = Convert.ToInt32(receipt["status"]);
var price = Convert.ToDouble(receipt["price"]);
if (status != 1) {
throw new Exception("결제 완료 상태가 아닙니다");
}
if (price != order.Amount) {
throw new Exception("결제 금액이 주문 금액과 다릅니다");
}
order.BootpayReceiptId = receipt["receipt_id"].ToString();
order.PaidAmount = price;
order.PaymentStatus = status;
order.Status = "done";
await orderRepository.SaveAsync(order);csharp응답 예시
결제 조회 응답은 결제 상태에 따라 status와 일부 필드가 달라져요. 주문 확정은 보통 status: 1일 때만 처리하고, 나머지는 대기·취소·실패 상태로 분기해요. method와 method_origin은 한글 라벨, method_symbol과 method_origin_symbol은 영문 심볼로 내려와요. SDK 4.1.3 이하로 요청된 결제 건은 한글 라벨이 이전 명칭(예: 카드결제)으로 내려올 수 있어요.
{
"receipt_id": "6244f60c1fc19202e42e8c4e",
"order_id": "order_20260428_001",
"price": 1000,
"tax_free": 0,
"cancelled_price": 0,
"cancelled_tax_free": 0,
"order_name": "결제 테스트 상품",
"company_name": "부트페이",
"gateway_url": "https://gw.bootpay.co.kr",
"metadata": {
"order_source": "web"
},
"sandbox": true,
"pg": "kcp",
"method": "카드",
"method_symbol": "card",
"method_origin": "카드",
"method_origin_symbol": "card",
"currency": "KRW",
"status": 1,
"status_locale": "결제완료",
"receipt_url": "https://receipt.bootpay.co.kr/receipt/6244f60c1fc19202e42e8c4e",
"purchased_at": "2026-04-28T09:30:29+09:00",
"requested_at": "2026-04-28T09:30:04+09:00",
"card_data": {
"tid": "20260428123456789012",
"card_approve_no": "12345678",
"card_no": "1234-****-****-5678",
"card_quota": "00",
"card_company_code": "08",
"card_company": "하나카드",
"card_interest": "0",
"receipt_url": "https://receipt.pg.example/card/20260428123456789012"
}
}json{
"receipt_id": "6244f60c1fc19202e42e8c4e",
"order_id": "order_20260428_001",
"price": 1000,
"order_name": "결제 테스트 상품",
"pg": "kcp",
"method": "카드",
"method_symbol": "card",
"method_origin": "카드",
"method_origin_symbol": "card",
"currency": "KRW",
"status": 2,
"status_locale": "입금/승인대기",
"requested_at": "2026-04-28T09:30:04+09:00"
}json{
"receipt_id": "6244f60c1fc19202e42e8c4e",
"order_id": "order_20260428_001",
"price": 1000,
"order_name": "무통장 입금 주문",
"pg": "kcp",
"method": "가상계좌",
"method_symbol": "vbank",
"method_origin": "가상계좌",
"method_origin_symbol": "vbank",
"currency": "KRW",
"status": 5,
"status_locale": "가상계좌발급완료",
"vbank_data": {
"tid": "20260428123456789014",
"bank_code": "004",
"bank_name": "국민은행",
"bank_account": "1234567890",
"bank_username": "부트페이",
"sender_name": "홍길동",
"expired_at": "2026-04-29T23:59:59+09:00"
},
"requested_at": "2026-04-28T09:30:04+09:00"
}json{
"receipt_id": "6244f60c1fc19202e42e8c4e",
"order_id": "order_20260428_001",
"price": 0,
"cancelled_price": 1000,
"order_name": "결제 테스트 상품",
"pg": "kcp",
"method": "카드",
"method_symbol": "card",
"method_origin": "카드",
"method_origin_symbol": "card",
"currency": "KRW",
"status": 20,
"status_locale": "결제취소완료",
"cancelled_at": "2026-04-28T09:35:12+09:00",
"purchased_at": "2026-04-28T09:30:29+09:00"
}json{
"error_code": "RC_NOT_FOUND",
"message": "영수증 정보를 찾지 못했습니다."
}json결제수단별 상세 객체
공통 필드 뒤에는 실제 결제수단에 맞는 상세 객체가 추가돼요. 카드 결제는 card_data, 계좌이체는 bank_data, 가상계좌는 vbank_data, 휴대폰 결제는 phone_data를 확인해요. 카카오머니는 kakao_money_data, 네이버포인트는 naver_point_data, 페이코포인트는 payco_point_data, 토스포인트·토스머니는 toss_point_data, 현금영수증 단독 발행은 cash_receipt_data, 상품권(컬쳐랜드·해피머니·도서문화·스마트문상)은 gift_card_data로 내려와요. 사용하지 않은 결제수단의 객체는 응답에 없을 수 있고, 삼성페이 등 일부 수단은 상세 객체 없이 공통 필드만 내려올 수 있어요.
{
"card_data": {
"tid": "20260428123456789012",
"card_approve_no": "12345678",
"card_no": "1234-****-****-5678",
"card_quota": "00",
"card_company_code": "08",
"card_company": "하나카드",
"card_interest": "0",
"card_type": "신용",
"card_owner_type": "개인",
"card_bank_code": "081",
"card_src_code": "01",
"complex_payment": false,
"partial_cancel_able": true,
"point": 0,
"cancelled_point": 0,
"coupon": 0,
"cancelled_coupon": 0,
"currency": "KRW",
"event_code": "00",
"receipt_url": "https://receipt.pg.example/card/20260428123456789012"
}
}json{
"bank_data": {
"tid": "20260428123456789013",
"bank_code": "004",
"bank_name": "국민은행",
"bank_account": "9876****3210",
"bank_username": "홍길동",
"cash_receipt_tid": "20260428123456789021",
"cash_receipt_type": "소득공제",
"cash_receipt_no": "01012345678",
"receipt_url": "https://receipt.pg.example/bank/20260428123456789013",
"cash_receipt_url": "https://receipt.pg.example/cash/20260428123456789021"
}
}json{
"vbank_data": {
"tid": "20260428123456789014",
"bank_code": "004",
"bank_name": "국민은행",
"bank_username": "부트페이",
"bank_account": "1234567890",
"sender_name": "홍길동",
"expired_at": "2026-04-29T23:59:59+09:00",
"cash_receipt_tid": "20260428123456789020",
"cash_receipt_type": "소득공제",
"cash_receipt_no": "01012345678"
}
}json{
"phone_data": {
"tid": "20260428123456789015",
"carrier": "SKT",
"auth_no": "123456",
"phone": "010-****-5678",
"device": "mobile",
"event_code": "00",
"receipt_url": "https://receipt.pg.example/phone/20260428123456789015"
}
}json{
"kakao_money_data": {
"tid": "20260428123456789016",
"partial_cancel_able": true
}
}json{
"cash_receipt_data": {
"tid": "20260428123456789017",
"cash_receipt_type": "소득공제",
"cash_receipt_no": "01012345678",
"receipt_url": "https://receipt.pg.example/cash/20260428123456789017"
}
}json{
"gift_card_data": {
"tid": "20260428123456789018",
"cid": "CULTURE01",
"cash_receipt_type": "소득공제",
"cash_receipt_no": "01012345678",
"receipt_url": "https://receipt.pg.example/gift/20260428123456789018"
}
}jsoncard_data, bank_data, vbank_data, phone_data 안의 일부 필드는 PG사와 결제 상태에 따라 생략될 수 있어요. 카드번호와 휴대폰 번호는 PG 정책에 따라 마스킹된 값으로 내려오므로 원문 복원을 전제로 저장하면 안 돼요.
가상계좌 vbank_data는 상태에 따라 구성이 달라져요. 발급완료(status 5) 시점에는 위 예시의 발급 정보가 내려오고, 입금완료 이후(status 1·20)에는 receipt_url, cash_receipt_url, 취소 시 cancel_tid가 추가돼요. 현금영수증 관련 필드(cash_receipt_tid, cash_receipt_type, cash_receipt_no)는 bank_data·vbank_data 모두 현금영수증을 신청한 건에만 포함돼요.
취소된 결제 건에는 상세 객체에 cancel_tid가 추가돼요. card_data의 point·coupon은 복합결제에서 포인트·쿠폰으로 결제된 금액이고 partial_cancel_able은 부분취소 가능 여부, bank_data의 bank_account는 PG 정책에 따라 마스킹되거나 생략될 수 있어요. phone_data의 carrier는 통신사, device는 결제한 단말 정보예요.
naver_point_data, payco_point_data, toss_point_data는 kakao_money_data와 같은 구조(tid, partial_cancel_able, 취소 시 cancel_tid)예요.
결제창과 결제위젯의 프론트엔드 이벤트 데이터는 receipt_id를 받기 위한 최소 데이터예요. 주문 확정에 필요한 기준 JSON은 서버에서 GET /v2/receipt/{receipt_id}로 다시 조회한 이 응답이에요. 결제창, 결제위젯, 브랜드페이, 웹훅 처리 모두 같은 조회 응답을 기준으로 status, price, order_id를 내부 DB 값과 비교해요.
주문 완료 판단에 자주 쓰는 필드는 아래와 같아요.
| 필드 | 저장 여부 | 용도 |
|---|---|---|
receipt_id |
필수 | 결제 조회, 취소, 웹훅 중복 처리 기준 |
order_id |
필수 | 내부 주문과 Bootpay 결제 건 연결 |
price |
필수 | 결제 금액 기록과 금액 일치 여부 확인. 취소분을 차감한 후의 잔액이에요 (원 결제금액 − 누적 취소금액) |
status |
필수 | 결제 완료, 취소, 승인 대기 상태 판단 |
method_symbol |
권장 | 결제수단 표시와 운영 분석 |
method_origin |
선택 | 간편결제 안에서 실제 사용한 원 결제수단 구분 |
pg |
권장 | PG별 장애·정산 확인 |
purchased_at |
권장 | 결제 완료 시각 기록 |
cancelled_price |
권장 | 부분 취소 이후 남은 금액 계산 |
card_data.card_company |
선택 | 카드사명 표시와 운영 조회 |
card_data.card_approve_no |
선택 | 카드 승인번호 확인 |
아래 필드는 결제 유형과 SDK 버전에 따라 응답에 추가돼요. 값이 없는 필드는 응답에서 제거되므로, 필드 존재 여부를 전제로 파싱하면 안 돼요.
| 필드 | 포함 조건 |
|---|---|
reserve_id |
예약결제로 생성된 결제 건 |
cancel_id, last_cancelled_at |
SDK 2.5 이상이고 취소 이력이 있는 결제 건 |
widget, widget_sandbox, brandpay, widget_data |
v3 결제위젯·브랜드페이 결제 건 |
escrow_data |
에스크로 결제 건. status, status_locale, shipping_started_at, receipt_confirmed_at, receipt_revoked_at, escrow_confirm_url을 포함해요 |
user |
lookup_user_data=true로 조회한 경우 |
결제 status 값
결제 조회 응답의 status는 Bootpay 결제 상태예요. 주문 상태 문자열로 그대로 복사하기보다, 서버 모델의 receipt_status 상수와 status_locale 라벨을 기준으로 내부 주문 상태를 따로 전이해요.
| status | 상태 의미 | 서버 처리 기준 |
|---|---|---|
0 |
결제대기 | 아직 주문 완료 처리하지 않아요 |
1 |
결제완료 | 금액·주문번호까지 맞을 때만 주문을 완료 처리해요 |
2 |
입금/승인대기 | 서버 승인 흐름에서는 승인 API 호출 전 검증 기준으로, 일부 입금 대기 흐름에서는 대기 상태로 사용해요 |
3 |
결제승인중 | 잠시 뒤 다시 조회하거나 웹훅으로 보정해요 |
4 |
결제진행중 | 주문 완료 처리하지 않고 진행 상태로 둬요 |
5 |
가상계좌발급완료 | 입금 대기 상태로 저장하고, 입금 완료 웹훅(status: 1)에서 확정해요 |
6 |
중간 Blank 진입 상태 | 내부 중간 상태로 보고 주문 완료 처리하지 않아요 |
7 |
결제승인지연중 | 재조회 또는 웹훅으로 최종 상태를 보정해요 |
10 |
아이템 View | 결제 완료 기준으로 사용하지 않아요 |
11 |
빌링키발급완료 | 자동결제·예약결제용 빌링키 저장 흐름에서 사용해요 |
12 |
본인인증완료 | 본인인증 흐름에서만 완료 기준으로 사용해요 |
20 |
결제취소완료 | 내부 주문·환불 상태를 취소로 보정해요 |
21 |
취소처리지연중 | 최종 취소 완료 웹훅 또는 재조회 결과로 보정해요 |
30 |
결제취소진행중 | 최종 취소 완료 웹훅 또는 재조회 결과로 보정해요 |
40 |
자동결제준비 | 빌링키 발급 준비 상태로 보고 완료 처리하지 않아요 |
41 |
자동결제빌링키발급이전 | 추가 발급·확정 단계가 끝난 뒤 빌링키를 저장해요 |
60 |
현금영수증발행완료 | 현금영수증 부가 상태예요 |
61 |
현금영수증발행취소 | 현금영수증 부가 상태예요 |
-1 |
결제실패 | 실패 사유를 기록하고 주문 완료 처리하지 않아요 |
-2 |
결제승인실패 | 승인 실패로 기록하고 재시도/고객 안내를 처리해요 |
-3 |
가상계좌발급취소 | 입금 대기 주문을 취소 또는 만료 상태로 보정해요 |
-4 |
결제요청실패 | 결제 요청 실패로 기록해요 |
-5 |
승인치명적인오류 | 운영자가 확인할 수 있도록 로그와 알림을 남겨요 |
-11 |
빌링키발급취소 | 저장 결제수단 등록 실패 또는 해지로 처리해요 |
-15 |
닫힘 | 결제창 이탈로 보고 주문 완료 처리하지 않아요 |
-16 |
결제시간만료 | 결제 시간 만료로 기록하고 주문 완료 처리하지 않아요 |
-17 |
결제금액변조승인 | 금액 변조 의심 건으로 주문 완료 처리하지 않고 운영자가 확인해요 |
-20 |
결제취소실패 | 취소 재시도 또는 수동 확인 대상으로 남겨요 |
-21 |
치명적인 취소 실패 | 운영자가 확인할 수 있도록 로그와 알림을 남겨요 |
-30 |
결제취소진행중 | 취소 진행 상태로 보고 최종 결과를 다시 확인해요 |
-40 |
자동결제빌링키발급실패 | 빌링키 발급 실패로 기록하고 고객에게 재등록을 안내해요 |
-60 |
현금영수증발행실패 | 현금영수증 부가 상태예요 |
-61 |
현금영수증취소실패 | 현금영수증 부가 상태예요 |
-100 |
결제창닫힘 | 결제가 진행되지 않은 이탈 상태로 처리해요 |
DB 스키마는 데이터 모델 설계를 참고해요.
조회 결과 불일치 처리
조회 결과가 내부 주문 정보와 맞지 않으면 주문을 완료 처리하면 안 돼요. 실패 사유를 기록하고, 필요한 경우 취소 또는 고객 안내 흐름으로 보내요.
| 실패 상황 | 처리 |
|---|---|
receipt_id가 없음 |
잘못된 요청으로 처리해요 |
| 결제 내역을 찾을 수 없음 | 프론트엔드 요청 값 또는 웹훅 값을 다시 확인해요 |
| 금액 불일치 | 주문 완료 처리하지 않고 결제 취소를 검토한다 |
| 결제 미완료 상태 | 대기 또는 실패 상태로 저장한다 |
이미 처리한 receipt_id |
멱등 처리하고 중복 저장하면 안 돼요 |
금액이 다르거나 상태가 완료가 아닌 결제 건은 주문을 done으로 바꾸면 안 돼요.
주문 상태 변경은 반드시 서버 조회와 내부 주문 확인이 끝난 뒤 한 번만 수행해요.
에러 코드
인증·권한 관련 에러는 에러 코드표를 참고해요.
| 코드 | 메시지 | 대처 방법 |
|---|---|---|
RC_NOT_FOUND |
영수증 정보를 찾지 못했습니다 | receipt_id가 올바른지 확인해요 |
RC_NOT_PAYMENT |
receipt_id가 결제 정보가 아닙니다 | 본인인증·빌링키 발급 등 결제가 아닌 receipt_id로 결제 조회를 호출했어요. 용도에 맞는 조회 API를 사용해요 |
TOKEN_KEY_INVALID |
인증 토큰이 유효하지 않다 | 서버 인증 정보를 확인해요 |
APP_KEY_CHAIN_SESSION_INVALID |
Client Key 접근 권한 없음 | client_key와 secret_key가 올바른지 확인해요 |
