서버 연동

결제 조회

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

결제 조회는 프론트엔드에서 받은 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

서버 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 내부 주문 금액과 일치하는지 확인해요
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일 때만 처리하고, 나머지는 대기·취소·실패 상태로 분기해요.

{
  "receipt_id": "6244f60c1fc19202e42e8c4e",
  "order_id": "order_20260428_001",
  "price": 1000,
  "tax_free": 0,
  "cancelled_price": 0,
  "cancelled_tax_free": 0,
  "order_name": "결제 테스트 상품",
  "company_name": "부트페이",
  "sandbox": true,
  "pg": "kcp",
  "method": "card",
  "method_symbol": "card",
  "currency": "KRW",
  "status": 1,
  "status_locale": "결제완료",
  "purchased_at": "2026-04-28T09:30:29+09:00",
  "requested_at": "2026-04-28T09:30:04+09:00"
}json
결제창·결제위젯 결과와의 관계

결제창과 결제위젯의 프론트엔드 이벤트 데이터는 receipt_id를 받기 위한 최소 데이터예요. 주문 확정에 필요한 기준 JSON은 서버에서 GET /v2/receipt/{receipt_id}로 다시 조회한 이 응답이에요. 결제창, 결제위젯, 브랜드페이, 웹훅 처리 모두 같은 조회 응답을 기준으로 status, price, order_id를 내부 DB 값과 비교해요.

주문 완료 판단에 자주 쓰는 필드는 아래와 같아요.

필드 저장 여부 용도
receipt_id 필수 결제 조회, 취소, 웹훅 중복 처리 기준
order_id 필수 내부 주문과 Bootpay 결제 건 연결
price 필수 결제 금액 기록과 금액 일치 여부 확인
status 필수 결제 완료, 취소, 승인 대기 상태 판단
method_symbol 권장 결제수단 표시와 운영 분석
pg 권장 PG별 장애·정산 확인
purchased_at 권장 결제 완료 시각 기록
cancelled_price 권장 부분 취소 이후 남은 금액 계산

결제 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가 올바른지 확인해요
TOKEN_KEY_INVALID 인증 토큰이 유효하지 않다 서버 인증 정보를 확인해요
APP_KEY_CHAIN_SESSION_INVALID Client Key 접근 권한 없음 client_key와 secret_key가 올바른지 확인해요

다음 단계