서버 연동

결제 조회

서버에서 결제 내역을 조회하고 주문 완료 기준을 확인해요.

결제 조회는 프론트엔드에서 받은 receipt_id로 Bootpay 결제 내역을 서버에서 다시 조회하고, 내부 주문 정보와 비교하는 단계예요.

프론트엔드의 성공 콜백만으로 주문을 완료 처리하면 안 돼요. 브라우저에서 받은 값은 누락되거나 조작될 수 있으므로, 서버가 Bootpay API로 결제 상태와 금액을 다시 확인해야 해요.

이 문서는 서버에서 receipt_id로 결제 내역을 조회하고 내부 주문 정보와 비교하는 방법을 설명해요. 주문 상태를 done으로 바꾸기 전, 또는 웹훅으로 받은 이벤트를 처리하기 전에 같은 조회 흐름을 사용해요.


핵심 요약

  • 결제 조회 API는 receipt_id로 결제 내역을 조회해요.
  • 주문 완료 조건은 보통 상태가 결제 완료이고, 결제 금액이 내부 주문 금액과 일치하는 것​​이에요.
  • receipt_id, 결제 금액, 결제수단, 결제 완료 시각은 주문 DB에 저장해 둬요.
  • 웹훅을 받았을 때도 웹훅 데이터만 믿지 말고 같은 방식으로 다시 조회해요.
  • 금액이나 상태가 맞지 않으면 주문을 완료 처리하면 안 돼요.

API 엔드포인트

GEThttps://api.bootpay.co.kr/v2/receipt/{receipt_id}Basic Auth
파라미터 위치 필수 설명
receipt_id Path 필수 결제 완료, 승인 대기, 웹훅 이벤트에서 받은 Bootpay 영수증 ID
lookup_user_data Query 선택 true면 응답에 구매자 user 객체(결제창에서 수집된 username, phone, email 등)를 포함해요. 수집되지 않은 건은 usernull이에요

서버 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)
}javascript

응답 예시

결제 조회 응답은 결제 상태에 따라 status와 일부 필드가 달라져요. 주문 확정은 보통 status: 1일 때만 처리하고, 나머지는 대기·취소·실패 상태로 분기해요. methodmethod_origin은 한글 라벨, method_symbolmethod_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

결제수단별 상세 객체

공통 필드 뒤에는 실제 결제수단에 맞는 상세 객체가 추가돼요. 카드 결제는 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
PG별 응답 차이

card_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_datapoint·coupon은 복합결제에서 포인트·쿠폰으로 결제된 금액이고 partial_cancel_able은 부분취소 가능 여부, bank_databank_account는 PG 정책에 따라 마스킹되거나 생략될 수 있어요. phone_datacarrier는 통신사, device는 결제한 단말 정보예요. naver_point_data, payco_point_data, toss_point_datakakao_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가 올바른지 확인해요

다음 단계