체크아웃

서버 주문 준비

POST /v1/orders/prepare의 상품·금액·URL 응답과 고객 JWT를 이용한 호스팅 주문서 자동 로그인

POST https://api.bootapi.com/v1/orders/prepare는 주문서를 만들어요. 장바구니 견적의 quote_token을 보내는 방식과 기존 products·price 방식이 있어요. 금액은 어느 방식이든 서버가 계산해요. response_type: "url"이면 결제 페이지 URL을 반환해요. 요청·오류 전체는 주문 API를 확인해요.

요청

서버 Basic 인증을 사용해요. 브라우저·SDK는 Authorization 대신 공개 client_key로도 호출할 수 있지만, 이 경우 오류는 JSON이 아닌 HTTP 200 HTML로 와요. 회원 주문은 Bootpay-User-JWT 또는 secretKey 인증으로 지정한 user_id를 사용해요. client_key만으로 호출하면서 user_id를 보내면 API_PARAM_INVALID예요. 유효하지 않은 JWT와 user_id가 모두 없으면 비회원 주문으로 처리돼요.

새 연동은 먼저 POST /v1/cart/order-preview로 견적을 받고 quote_token을 전달해요. 견적과 주문 준비에서 같은 회원 지정 또는 같은 Bootpay-Device-UUID를 사용해야 해요. 구매자가 달라지면 ORDER_QUOTE_SESSION_MISMATCH, 상품 가격이나 배송비가 바뀌면 ORDER_QUOTE_PRICE_CHANGED로 거절돼요. 견적에 포함된 cart_item_id는 결제 완료 때 해당 회원 장바구니 수량을 차감하는 데 쓰여요.

{
  "quote_token": "q1.eyJ2Ijo...",
  "response_type": "url",
  "redirect_url": "https://shop.example.com/checkout/result"
}json

아래는 기존 연동을 위한 products·price 요청 예시예요.

{
  "products": [
    {
      "product_id": "PRODUCT_ID",
      "quantity": 2,
      "option": { "product_option_id": "OPTION_ID" }
    }
  ],
  "price": 20000,
  "uct": 1,
  "response_type": "url",
  "redirect_url": "https://shop.example.com/checkout/result",
  "use_auto_login": true
}json

옵션이 없으면 option을 생략해요. 장바구니 API와 달리 products[].option.product_option_id로 중첩​​해요. 구독 기간은 products[].period이며 단순히 subscription_period_id를 복사하지 않아요.

필드 설명
quote_token 견적 방식의 토큰. 보내면 상품·수량·배송 방식·금액은 서명된 견적 값이 기준이에요
user_id 회원 주문을 지정할 때 쇼핑몰 서버가 보내는 회원 ID. secretKey 인증에서만 사용해요. 자동 로그인 URL에는 회원 JWT가 필요해요
products 호환 방식에서 필수. 각 항목 product_id, 수량 quantity, 선택 option, period
price 호환 방식에서 필수. 배송비를 제외한 첫 결제 상품 금액이며 서버 재계산값과 비교해요
uct 회원 주문이면 필수. 1 개인, 2 기업, 3 부서. 비회원 주문에서는 사용하지 않아요
delivery_type 선택 배송 유형
invoice_id 선택 청구서 ID. 현재 프로젝트의 청구서여야 하며 없으면 주문서 생성 전에 INVOICE_NOT_FOUND예요. 청구서 API 참고
response_type url이면 결제 URL 응답, 그 외에는 주문서 메타데이터
redirect_url 결제 후 돌아올 가맹점 URL. 견적 방식에서는 쇼핑몰에 등록된 HTTPS 도메인만 허용해요
use_auto_login 기본 false. URL 모드에서 true이면 API가 세션 경유 /b/{client_key} URL을 반환해요. 유효한 Bootpay-User-JWT가 필수이며 user_id만으로는 USER_SESSION_INVALID예요

견적 방식에서 products·price를 함께 보내면 서버는 견적과 같은지만 확인해요. 값이 다르면 ORDER_QUOTE_TOKEN_INVALID예요. 호환 방식은 서버가 상품 가격을 재계산하고 price가 다르면 ORDER_PRICE_NOT_MATCH예요. 배송비를 포함한 summary.total_order_price를 호환 방식의 price에 넣지 않아요.

Idempotency-Key를 보내면 같은 구매자의 같은 입력으로 재시도할 때 원래 주문 준비 응답을 돌려줘요. 같은 키에 다른 입력을 보내면 IDEMPOTENCY_REQUEST_MISMATCH, 처리 중이면 IDEMPOTENCY_REQUEST_IN_PROGRESS와 Retry-After: 1이 와요. 키 없이 재시도하면 새 주문서가 만들어져요.

const prepared = await commerce.post('orders/prepare', {
  products: [{ product_id: productId, quantity: 2,
    option: { product_option_id: optionId } }],
  price: serverCalculatedProductPrice,
  uct: 1,
  response_type: 'url',
  redirect_url: 'https://shop.example.com/checkout/result',
  use_auto_login: true
}, {
  headers: { 'Bootpay-User-JWT': customerJwt }
})javascript

다른 도메인의 주문서에서도 로그인 유지하기

Next.js 쇼핑몰의 쿠키는 shop.bootpay.co.kr 또는 <subdomain>.bootpay.shop로 전달되지 않아요. 회원 자동 로그인은 API가 기존 /b/{client_key} 세션 경유 URL을 반환하고 브라우저가 그 URL로 이동하는 방식​​으로 연결해요. SDK·MCP·가맹점이 토큰을 암호화하거나 경유 URL을 조합할 필요는 없어요.

  1. BFF가 현재 로그인한 고객의 Commerce JWT를 확보해요. 비밀번호 로그인으로 발급받은 JWT가 있으면 그대로 사용해요. 자사 인증만 있다면 서버가 검증한 고객 매핑으로 로그인 토큰을 발급해요.
  2. 같은 JWT로 GET /v1/users/session을 호출하고 응답의 user_id를 확인해요. 200이어도 본문이 null이면 유효한 회원 세션이 아니에요.
  3. POST /v1/orders/prepare를 호출할 때 요청 헤더​​에 Bootpay-User-JWT, 본문​​에 response_type: 'url'과 use_auto_login: true를 함께 넣어요. redirect_url은 결제 후 돌아올 자사 URL이에요.
  4. API가 고객 세션과 전달된 JWT를 검증한 뒤 주문서를 만들고, { order_number, url }의 url에 세션 경유 /b/{client_key} 주소를 넣어요. API는 검증한 요청의 고객 JWT로 경유 세션을 구성해요.
  5. BFF는 호스트와 경유 URL의 목적지를 검증하고 응답 url을 그대로 브라우저에 전달해요. 브라우저가 /b로 이동하면 호스팅이 자체 세션 쿠키를 설정한 뒤 주문 페이지로 이동해요. 주문 페이지는 회원 세션 조회와 주문 소유자 확인을 완료한 뒤 주소·지갑·결제 정보를 불러와요.

JWT는 BFF에서 API로 전달해요. 가맹점이 url 뒤에 JWT를 추가하거나 자사 쿠키의 도메인을 바꾸지 않아요. API가 만든 __s는 암호화된 세션 자격증명이므로 복호화·재암호화·로그 출력을 하지 않고 그대로 전달해요. 임베드 위젯의 updateUserSession 이벤트는 별도 연동 방식이에요.

주문 생성 당시 상태 호스팅 주문서의 자동 로그인
URL 모드 + 인증된 고객 JWT + use_auto_login: true API가 /b/{client_key} 세션 경유 URL을 반환
URL 모드 + 필드 생략 또는 false 기존 직접 주문 URL 반환. 자동 로그인 보장 없음
URL 모드 + 고객 JWT 없음/무효 또는 고객 식별 실패 + use_auto_login: true 주문 생성 전 HTTP 401 USER_SESSION_INVALID(420). 비회원으로 조용히 전환하지 않음
비URL 모드 기존 주문 메타데이터 응답 유지. /b URL 응답 계약은 URL 모드에 적용

use_auto_login은 주문 생성 시 저장돼요. 설정을 수정한 뒤에는 새 주문 준비 응답으로 확인해요. 이전에 생성한 URL을 다시 열어도 그 주문의 저장된 설정과 구매자가 자동으로 바뀌지 않아요.

일반 주문 URL과 청구서 세션 경유 URL

일반 준비 주문도 기존 범용 세션 경유 /b/{client_key}​​를 사용할 수 있어요. response_type: 'url'과 use_auto_login: true이면 API가 이 URL을 완성해 반환해요. 기존 청구서 전용 /i/.../session과는 다른 경로예요.

구분 진입 경로와 식별자 세션 처리
회원 자동 로그인 OrderPre API가 반환한 /b/{client_key}?__s=...&redirect_url=... 호스팅이 암호화된 세션을 적용한 뒤 같은 호스트의 주문 URL로 이동
자동 로그인 비활성화 OrderPre API가 반환한 /mall/{mall_key}/order/{order_number} 기존 직접 주문 URL. 별도 로그인 상태 필요
기존 청구서 Invoice /i/{clientKey}/{invoiceId}/session의 invoiceId는 Invoice 문서 BSON ID 청구서 화면이 GET /mall/invoice/session/{invoiceId}를 호출하고 응답의 redirect_url로 이동

일반 주문번호나 OrderPre 문서 ID를 청구서 세션 API의 invoiceId로 전달하지 않아요. 주문 URL에 /session을 붙이지 않아요. 가맹점은 /b도 직접 만들지 않고 API의 url만 사용해요. 고객 로그인 토큰은 BFF가 고객 JWT를 확보하는 API예요.

응답

회원 자동 로그인 URL 응답의 형태:

{
  "order_number": "26091412345678901234",
  "url": "https://shop.bootpay.co.kr/b/CLIENT_KEY?__s=ENCRYPTED_SESSION&redirect_url=ENCODED_ORDER_PATH"
}json

위 문자열은 형태 설명용이며 실제 응답을 그대로 사용해요. BFF는 허용한 HTTPS 호스트인지 검증하고, /b의 redirect_url도 같은 호스트의 주문 경로로 해석되는지 확인해요. API는 기존 주문 URL의 상대 경로와 쿼리를 목적지로 인코딩해요. __s의 인코딩을 다시 쓰거나 쿼리를 제거하지 않아요. 이 응답에는 order_id가 없어요. 요청 본문의 redirect_url은 결제 후 자사 복귀 URL이고, /b 쿼리의 redirect_url은 세션 설정 후 진입할 Bootpay 주문 경로예요.

최상위 token 필드는 추가되지 않아요. 자동 로그인 세션은 API가 만든 URL의 __s에 암호화되어 있으므로 URL 자체가 민감한 자격증명이에요. 원문 URL·__s·JWT를 공개 문서·분석 로그·공유 게시물에 남기지 않고, 프록시나 클라이언트에서 복호화·재조합하지 않아요.

기본 응답:

{
  "order_id": "ORDER_PRE_DOCUMENT_ID",
  "order_number": "26091412345678901234",
  "order_name": "상품명",
  "price": 20000,
  "expired_at": "2026-09-14T12:00:00+09:00"
}json

order_id는 준비 주문서 문서 ID, order_number는 주문번호예요. 주문 조회·결제 승인에는 주문번호를 사용해요. 견적 방식 비회원의 비URL 응답에만 result_token과 result_token_expires_at이 추가돼요. 구매자 결과 조회에 24시간 동안 사용해요.

완료 검증과 현재 범위

  1. BFF는 생성한 주문번호를 로그인 세션/구매 시도와 연결해 저장해요.
  2. 구매자가 반환된 결제 페이지에서 결제를 진행해요.
  3. 복귀 URL과 브라우저 성공 메시지만으로 완료 처리하지 않아요.
  4. 서버가 GET /v1/orders/:order_number로 다시 조회하고 구매자·금액·status·receipt_status를 검증해요.
  5. 웹훅으로 지연 입금·취소 상태를 보정해요. 웹훅 테스트 API로 테스트 이벤트를 보낼 수 있어요.

POST /v1/order/confirm은 결제 승인이므로 견적·준비 단계에서 호출하지 않아요. 배송지 입력/변경, 쿠폰·적립금 적용, 결제수단 요청까지 모두 자체 화면으로 처리하는 완전한 결제 폼은 추가 v1 계약이 필요해요. 이 문서의 기본 구현 경로는 서버에서 주문서를 만들고 제공된 결제 URL로 이동하는 방식이에요.

오류로는 ORDER_PRODUCT_NOT_FOUND, ORDER_OPTION_NOT_FOUND, ORDER_PRODUCT_NEED_OPTION, ORDER_PRODUCT_STOCK_OUT, ORDER_OPTION_STOCK_OUT, ORDER_PRICE_NOT_MATCH 등이 있어요. 실제 결제 준비 POST는 주문서를 생성하는 변경 작업이에요.

결제 페이지에서 로그인이 풀려 보일 때

새로운 인증 API가 필요한지 판단하기 전에 아래 순서로 확인해요. 원문 JWT·secret·자동 로그인 토큰은 로그에 남기지 않아요.

  1. 화면 문구와 실제 오류 분리: "세션 만료"라는 제목만 보고 인증 실패로 판단하지 않아요. 응답의 오류 코드와 서버 주문 상세의 status, receipt_status를 먼저 확인해요. ORDER_NOT_PAYMENT_READY는 결제 가능한 주문 상태가 아니라는 뜻이며, 로그인 토큰 재발급만으로 해결되지 않아요.
  2. 자사 로그인과 회원 세션 유효성: BFF가 이번 요청의 JWT를 읽었는지, 같은 프로젝트와 JWT로 GET /v1/users/session을 호출했을 때 user_id가 있는지 확인해요. 만료·무효이면 재로그인이 필요해요.
  3. 실제 SDK 전송값: prepare의 헤더에 Bootpay-User-JWT가 있고 본문에 response_type: 'url', use_auto_login: true가 있는지 확인해요. setToken(jwt)는 고객 헤더를 설정하지 않아요.
  4. 주문 생성 시 고객 연결: 신뢰할 수 있는 서버 조회/관리 도구에서 해당 주문의 구매자를 확인해요. 일반 주문 상세 응답에 use_auto_login이 노출되지 않으면 응답에 그 필드가 없다는 이유로 저장값을 false라고 판단하지 않아요. 생성 요청 기록이나 별도로 권한 있는 설정 조회가 필요해요. secretKey 인증의 user_id는 회원 주문 지정에 쓸 수 있지만 자동 로그인 URL에는 유효한 JWT가 필요해요.
  5. API 반환 경로: URL 모드 + 자동 로그인 요청인데 응답이 직접 /mall/.../order/...이면 /b 반환 계약의 누락이에요. 가맹점이 경유 URL을 직접 만들지 말고 API 구현·배포를 확인해요. 유효한 주문 상세의 임시 토큰만 존재한다고 브라우저의 로그인 인계가 완료된 것은 아니에요.
  6. 경유 후 세션과 배포: 응답 URL을 그대로 이동했을 때 /b가 호스팅 쿠키를 설정하고, 주문 페이지의 세션 조회에서 주문 소유자가 확인되는지 검증해요. 자격증명 값 대신 경로 종류·쿠키 존재 여부·소유자 일치 여부만 기록해요. API·호스팅의 배포 버전과 기존 주문 여부를 구분하고, 주문 만료/결제 불가라면 주문 상태를 별도로 처리해요.

만료된 주문서로 다시 이동하지 않기

status: 'order_expired'와 receipt_status: 'receipt_ready'는 함께 나타날 수 있어요. 영수증이 준비 상태라는 이유만으로 주문서도 계속 결제 가능하다고 판단하지 않아요. ORDER_EXPIRED는 만료 오류이고, ORDER_NOT_PAYMENT_READY는 만료를 포함해 결제를 진행할 수 없는 주문 상태에서 발생할 수 있으므로 서버 주문 상세로 구분해요. 알 수 없는 오류와 통신 실패는 확인 실패로 표시해요.

BFF가 구매 시도 ID에 주문번호·URL을 저장해 재사용한다면, 저장된 URL을 반환하기 전에 같은 고객의 기록인지 확인하고 commerce.order.detail(orderNumber)로 현재 상태를 다시 조회해요.

  • 현재 주문이 결제 가능한 상태임을 확인한 경우에만 기존 구매 시도의 URL을 재사용해요.
  • 명시적으로 order_expired임을 확인하면 기존 URL 재전송을 중단하고 고객에게 "새 주문서로 다시 진행"을 제공해요. 고객이 선택하면 새 구매 시도 ID로 상품·재고·금액을 다시 검증하고 새 주문서를 만들어요.
  • 결제 완료·승인/결제 처리 중·상태 불명·조회 실패에는 자동으로 새 구매 시도 ID를 발급하거나 주문서를 생성하지 않아요. 기존 주문 결과를 확인해 중복 주문·결제를 방지해요.

세션 복원은 만료된 주문서를 연장하지 않아요. 새 주문 생성 재시도와 로그인 복원은 별도의 처리예요. Next.js BFF 재시도 경계를 함께 확인해요.

확인 근거: V1::OrdersController#prepare, V1::OrderPrepareService, Order::Save#create_by_mall, Order::DataFormat#detail_data_with_javascript_key, Order::Authenticate#create_auto_login_session, invoice-frontend/app/pages/b/[clientKey].vue, invoice-frontend/app/vendor/composables/http/http-environment.ts, invoice-frontend/app/stores/commerce/mall/order-pre.ts.