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을 조합할 필요는 없어요.
- BFF가 현재 로그인한 고객의 Commerce JWT를 확보해요. 비밀번호 로그인으로 발급받은 JWT가 있으면 그대로 사용해요. 자사 인증만 있다면 서버가 검증한 고객 매핑으로 로그인 토큰을 발급해요.
- 같은 JWT로
GET /v1/users/session을 호출하고 응답의user_id를 확인해요.200이어도 본문이null이면 유효한 회원 세션이 아니에요. POST /v1/orders/prepare를 호출할 때 요청 헤더에Bootpay-User-JWT, 본문에response_type: 'url'과use_auto_login: true를 함께 넣어요.redirect_url은 결제 후 돌아올 자사 URL이에요.- API가 고객 세션과 전달된 JWT를 검증한 뒤 주문서를 만들고,
{ order_number, url }의url에 세션 경유/b/{client_key}주소를 넣어요. API는 검증한 요청의 고객 JWT로 경유 세션을 구성해요. - 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"
}jsonorder_id는 준비 주문서 문서 ID, order_number는 주문번호예요. 주문 조회·결제 승인에는 주문번호를 사용해요. 견적 방식 비회원의 비URL 응답에만 result_token과 result_token_expires_at이 추가돼요. 구매자 결과 조회에 24시간 동안 사용해요.
완료 검증과 현재 범위
- BFF는 생성한 주문번호를 로그인 세션/구매 시도와 연결해 저장해요.
- 구매자가 반환된 결제 페이지에서 결제를 진행해요.
- 복귀 URL과 브라우저 성공 메시지만으로 완료 처리하지 않아요.
- 서버가
GET /v1/orders/:order_number로 다시 조회하고 구매자·금액·status·receipt_status를 검증해요. - 웹훅으로 지연 입금·취소 상태를 보정해요. 웹훅 테스트 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·자동 로그인 토큰은 로그에 남기지 않아요.
- 화면 문구와 실제 오류 분리: "세션 만료"라는 제목만 보고 인증 실패로 판단하지 않아요. 응답의 오류 코드와 서버 주문 상세의
status,receipt_status를 먼저 확인해요.ORDER_NOT_PAYMENT_READY는 결제 가능한 주문 상태가 아니라는 뜻이며, 로그인 토큰 재발급만으로 해결되지 않아요. - 자사 로그인과 회원 세션 유효성: BFF가 이번 요청의 JWT를 읽었는지, 같은 프로젝트와 JWT로
GET /v1/users/session을 호출했을 때user_id가 있는지 확인해요. 만료·무효이면 재로그인이 필요해요. - 실제 SDK 전송값: prepare의 헤더에
Bootpay-User-JWT가 있고 본문에response_type: 'url',use_auto_login: true가 있는지 확인해요.setToken(jwt)는 고객 헤더를 설정하지 않아요. - 주문 생성 시 고객 연결: 신뢰할 수 있는 서버 조회/관리 도구에서 해당 주문의 구매자를 확인해요. 일반 주문 상세 응답에
use_auto_login이 노출되지 않으면 응답에 그 필드가 없다는 이유로 저장값을false라고 판단하지 않아요. 생성 요청 기록이나 별도로 권한 있는 설정 조회가 필요해요. secretKey 인증의user_id는 회원 주문 지정에 쓸 수 있지만 자동 로그인 URL에는 유효한 JWT가 필요해요. - API 반환 경로: URL 모드 + 자동 로그인 요청인데 응답이 직접
/mall/.../order/...이면/b반환 계약의 누락이에요. 가맹점이 경유 URL을 직접 만들지 말고 API 구현·배포를 확인해요. 유효한 주문 상세의 임시 토큰만 존재한다고 브라우저의 로그인 인계가 완료된 것은 아니에요. - 경유 후 세션과 배포: 응답 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.