에스크로 연동은 ① 결제창을 에스크로로 띄우고, ② 발송 시 배송정보를 전송하고, ③ 구매확정/거절 통지를 처리하는 세 단계로 이뤄져요. 전체 개념과 상태값은 에스크로 개요를 먼저 확인해요.
1에스크로 결제 요청 (프론트엔드)
일반 결제 요청에 extra.escrow: true만 추가하면 에스크로 결제가 돼요. 배송지가 필요하므로 user에 주소 정보를 함께 보내요.
import { Bootpay } from '@bootpay/client-js'
async function requestEscrowPayment() {
const response = await Bootpay.requestPayment({
client_key: '[ Client Key ]',
pg: '이니시스',
method: 'card',
order_name: '원목 식탁',
order_id: 'order_' + Date.now(),
price: 150000,
user: {
username: '홍길동',
phone: '01012345678',
addr: '서울특별시 종로구 ...', // 에스크로는 배송지 정보가 필요해요
},
extra: {
escrow: true, // 에스크로 결제 활성화
},
})
if (response.event === 'done') {
// 결제 완료 → 서버 검증 후 '배송 준비' 상태로 주문 저장
await verifyPayment(response.receipt_id)
}
}javascript에스크로는 결제 완료(done) 후 배송정보 전송 → 구매확정까지 가야 정산돼요. 결제 검증 단계에서 주문을 done이 아니라 배송 준비 같은 중간 상태로 저장하고, 발송 시 배송정보를 전송하도록 설계해요.
결제 요청 파라미터 전반은 결제창 통합 가이드와 같아요. 서버 검증은 결제 조회를 참고해요.
2배송정보 전송 (서버)
상품을 발송하면 택배사·송장번호·배송지를 등록해요. 이 호출로 에스크로 상태가 배송시작(3)으로 바뀌고, 구매자에게 구매확정 안내가 전달돼요.
https://api.bootpay.co.kr/v2/escrow/shipping/start/{receipt_id}Basic Auth 또는 Bearer 액세스 토큰Basic Auth 로 호출하려면 해당 연동키에 escrow_shipping_start 스코프가 부여돼 있어야 해요. 없으면
API_SCOPE_INVALID (HTTP 401) 로 거부돼요. 관리자 콘솔의 연동키 권한 설정에서 확인해요.
셀러(판매자) 계정 전용이라 리셀러·파트너 키로는 API_ONLY_SELLER 가 떨어져요.
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
receipt_id |
Path | 필수 | 에스크로 결제의 영수증 ID |
tracking_number |
String | 필수 | 송장(운송장) 번호 |
delivery_corp |
String | 필수 | 택배사명. PG별로 인식하는 이름이 달라요 — 아래 표 참고 |
shipping_prepayment |
Boolean | 선택 | 배송비 선결제 여부. 현재 구현에서는 false가 무시돼 항상 선불(SH)로 전송돼요 |
shipping_day |
Integer | 선택 | 배송 예정일 (일 단위). 생략하면 이니시스 기준 5 로 전송돼요 |
redirect_url |
String | 선택 | 구매자가 구매확정/거절을 끝낸 뒤 돌아갈 URL. 여기서 등록한 값이 구매확정 화면에 쓰여요 |
user.username |
String | 선택 | 수령인 이름. 생략하면 결제 시 저장된 구매자 정보를 그대로 써요 |
user.phone |
String | 선택 | 수령인 전화번호. 생략하면 결제 시 저장된 값을 써요 |
user.address |
String | 선택 | 배송지 주소. 생략하면 결제 시 같은 address 키로 저장된 값만 폴백돼요 |
user.zipcode |
String | 선택 | 배송지 우편번호. 생략하면 결제 시 같은 zipcode 키로 저장된 값만 폴백돼요 |
company.name |
String | 선택 | 발송처(판매자) 이름. 생략하면 가맹점 등록 상호가 쓰여요 |
company.phone |
String | 선택 | 발송처 전화번호. 생략하면 가맹점 등록 번호가 쓰여요 |
company.zipcode |
String | 선택 | 발송처 우편번호. 현재 이니시스 전송에서는 반영되지 않고 항상 가맹점 등록 우편번호가 쓰여요 |
company.addr1 |
String | 선택 | 발송처 주소. 생략하면 가맹점 등록 주소가 쓰여요 |
company.addr2 |
String | 선택 | 발송처 상세주소. 생략하면 가맹점 등록 상세주소가 쓰여요 |
이니시스가 인식하는 delivery_corp 값: CJ대한통운 한진 롯데 KGB 우체국 로젠 CJGLS
대신 일양로지스 호남 합동 천일 편의점 경동 기타
KCP가 인식하는 값: 위 목록에 동부 옐로우캡 사가와 직접배달 퀵서비스 추가 (CJGLS 는 없음)
매핑에 없는 택배사명을 보내도 오류가 나지 않고 이니시스는 9999(기타), KCP는 ETC(기타)로 전송돼요.
구매자가 송장 조회를 못 하게 되니 위 목록의 이름을 정확히 써요.
shipping_prepayment: false를 보내도 현재 구현에서는 값이 무시되고 항상 선불(SH)로 이니시스에 전송돼요.
착불 배송은 이 API로 표시할 수 없으니, 필요하면 가맹점 주문 정보에 따로 기록해요.
이니시스 에스크로는 배송시작 시 수령인 이름·전화·주소·우편번호가 비어 있으면
RC_USER_NAME_BLANK (2713) / RC_USER_PHONE_BLANK (2714) / RC_USER_ADDRESS_BLANK (2715) / RC_USER_ZIPCODE_BLANK (2716)
으로 거부돼요. 폴백은 결제 요청에서 같은 키 이름(address·zipcode) 으로 보낸 값만 되므로,
결제 때 addr 같은 다른 이름으로 보냈다면 이 요청에서 반드시 user.address·user.zipcode를 채워 보내요.
이니시스 배송시작은 요청에 company를 보냈는지와 무관하게 관리자 콘솔에 등록된 가맹점 정보를 먼저 검사해요.
상호·주소1·주소2·우편번호·대표번호가 모두 채워져 있어야 하고, 하나라도 비면 아래 오류로 거부돼요.
| 코드 | 메시지 |
|---|---|
RC_PROVIDER_NAME_BLANK (2708) |
배송정보에 등록할 회사명이 입력되지 않았습니다. 부트페이 관리자에서 입력해주세요. |
RC_PROVIDER_ADDR1_BLANK (2709) |
배송정보에 등록한 회사 주소가 입력되지 않았습니다. 부트페이 관리자에서 입력해주세요. |
RC_PROVIDER_ADDR2_BLANK (2710) |
배송정보에 등록할 회사 나머지 주소가 입력되지 않았습니다. 부트페이 관리자에서 입력해주세요. |
RC_PROVIDER_ZIP_BLANK (2711) |
배송정보에 등록할 회사 우편번호 정보가 입력되지 않았습니다. 부트페이 관리자에서 입력해주세요. |
RC_PROVIDER_PHONE_BLANK (2712) |
배송정보에 등록할 회사 대표번호 정보가 입력되지 않았습니다. 부트페이 관리자에서 입력해주세요. |
KCP 배송정보 전송은 결제 TID·송장번호·택배사만 보내요. 그래서 company뿐 아니라 shipping_prepayment·shipping_day·user 값도 사용되지 않고,
위의 수령인 정보 검증(RC_USER_*)과 가맹점 정보 검증(RC_PROVIDER_*)도 이니시스에서만 수행돼요.
curl -X PUT "https://api.bootpay.co.kr/v2/escrow/shipping/start/RECEIPT_ID" \
-H "Authorization: Basic $(printf '%s' 'CLIENT_KEY:SECRET_KEY' | base64)" \
-H "Content-Type: application/json" \
-d '{
"tracking_number": "123456789",
"delivery_corp": "CJ대한통운",
"shipping_day": 2,
"user": {
"username": "홍길동",
"phone": "01012345678",
"address": "서울특별시 종로구 ...",
"zipcode": "03000"
},
"company": {
"name": "부트가구",
"phone": "0233334444",
"addr1": "서울특별시 종로구 ...",
"addr2": "종로빌딩 3층"
}
}'bashimport { Bootpay } from '@bootpay/backend-js'
Bootpay.setConfiguration({
client_key: process.env.BOOTPAY_CLIENT_KEY,
secret_key: process.env.BOOTPAY_SECRET_KEY,
})
const response = await Bootpay.shippingStart({
receipt_id: '[ receipt_id ]',
tracking_number: '123456789',
delivery_corp: 'CJ대한통운',
shipping_day: 2,
user: {
username: '홍길동',
phone: '01012345678',
address: '서울특별시 종로구 ...',
zipcode: '03000',
},
company: {
name: '부트가구',
phone: '0233334444',
addr1: '서울특별시 종로구 ...',
addr2: '종로빌딩 3층',
},
})
console.log(response.escrow_data.status) // 3: 배송시작javascript응답 예시
배송정보 전송 응답은 결제 조회와 동일한 영수증 전체 payload 이며, 그 안에
escrow_data 가 포함돼요.
{
"receipt_id": "6244f60c1fc19202e42e8c4e",
"order_id": "order_1700000000",
"price": 150000,
"tax_free": 0,
"order_name": "원목 식탁",
"company_name": "부트가구",
"method": "카드",
"method_symbol": "card",
"purchased_at": "2026-06-13T11:02:00+09:00",
"requested_at": "2026-06-13T11:01:41+09:00",
"status_locale": "결제완료",
"status": 1,
"currency": "KRW",
"escrow_data": {
"status": 3,
"status_locale": "배송시작",
"shipping_started_at": "2026-06-14T14:13:43+09:00",
"escrow_confirm_url": "https://door.bootpay.co.kr/escrow/..."
}
}jsonescrow_data 필드는 다음과 같아요.
| 필드 | 타입 | 설명 |
|---|---|---|
status |
Number | 에스크로 상태값 |
status_locale |
String | 상태 한글명 |
shipping_started_at |
String | 배송시작 시각 (ISO8601) |
receipt_confirmed_at |
String | 구매확정 시각 (ISO8601) |
receipt_revoked_at |
String | 구매거절 시각 (ISO8601) |
escrow_confirm_url |
String | 이니시스 + 배송시작 상태일 때만 내려오는 구매확정 페이지 URL |
escrow_data 는 값이 없는 항목을 null 로 채우지 않고 키를 제거해서 내려줘요. receipt_confirmed_at
같은 필드는 undefined 체크로 읽어요.
드물게 escrow_data 자체가 없거나 null 로 내려오는 경우도 있으니, 하위 필드를 읽기 전에 escrow_data 존재 여부부터 확인하는 방어적 처리를 해요.
3구매확정 / 거절 통지
배송이 시작되면 구매자가 상품을 받고 구매확정 또는 거절(반품)을 선택해요. 상태 변경은 두 경로로 통지돼요.
- 웹훅: 에스크로 상태가 바뀌면 등록한 웹훅 URL로 통지돼요. 배송정보 전송·구매확정·구매거절·거절승인 모두 이 웹훅으로 통지돼요. 페이로드는 결제 웹훅과 같은 영수증 전체 payload 에
webhook_type: "ESCROW_STATUS_CHANGED",application_id,escrow_webhook: true,escrow_data가 더해진 형태이고, 빌링키로 결제한 건이면billing_key에 빌링키 ID 가 담겨요. 에스크로 상태값은escrow_data.status하나만 보면 돼요 — 최상위에는 상태 필드가 없어요. webhook_type은 API 버전 2.5 이상에서만 채워져요: 에스크로 웹훅은 결제 웹훅과 달리 빈 값을 걸러내지 않아서, 2.5 미만 프로젝트에서는webhook_type키가 빠지는 대신null로 내려와요(billing_key도 빌링 건이 아니면null).webhook_type만 보고 분기하면 에스크로 웹훅을 놓칠 수 있으니escrow_webhook: true도 함께 확인해요.- 결제 조회: 언제든
GET /v2/receipt/{receipt_id}로 현재escrow_data.status를 확인할 수 있어요.
{
"webhook_type": "ESCROW_STATUS_CHANGED",
"receipt_id": "6244f60c1fc19202e42e8c4e",
"order_id": "order_1700000000",
"price": 150000,
"order_name": "원목 식탁",
"status_locale": "결제완료",
"status": 1,
"application_id": "65a1c0ab8f1b5b00367a0003",
"escrow_webhook": true,
"escrow_data": {
"status": 1,
"status_locale": "구매확정",
"shipping_started_at": "2026-06-14T14:13:43+09:00",
"receipt_confirmed_at": "2026-06-16T10:02:11+09:00"
}
}json에스크로 상태 웹훅 역시 위변조될 수 있어요. receipt_id로 결제 조회를 다시 호출해 escrow_data.status를 확인한 뒤 정산·환불 흐름을 진행해요.
상태별 처리
| escrow status | 의미 | 가맹점이 할 일 |
|---|---|---|
3 |
배송시작 | 배송 중으로 주문 표시 |
1 |
구매확정 | 정산 확정 처리, 주문 완료 |
-2 |
구매거절요청 | 반품/환불 흐름 진행, 재고 복구 |
-3 |
구매거절승인 | 거절이 승인되어 환불 확정. 주문을 환불 완료로 보정 |
-1 |
정산보류 | 운영팀 확인 (분쟁 등) |
에러 코드
인증·권한 관련 에러는 에러 코드표를 참고해요.
| 코드 | 메시지 | 대처 방법 |
|---|---|---|
RC_NOT_FOUND (2000) |
영수증 정보를 찾지 못했습니다. | receipt_id가 올바른지 확인해요 |
RC_NOT_ESCROW (2703) |
에스크로 결제건이 아닙니다. | 결제 시 extra.escrow: true로 요청했는지, PG가 에스크로를 지원하는지 확인해요 |
RC_NOT_CONFIRMED (2069) |
결제 완료상태건이 아닙니다. | 결제가 완료(status: 1)된 뒤에 호출해요 |
RC_ESCROW_NOT_READY (2700) |
에스크로 대기 상태가 아니면 배송 시작을 할 수 없습니다. | escrow_data.status 가 0 일 때만 호출해요. 중복 호출을 막아요 |
RC_TRACKING_NUMBER_BLANK (2706) |
tracking_number (운송장 번호) 값을 입력해주세요. | tracking_number 를 채워 보내요 |
RC_DELIVERY_CORP_BLANK (2707) |
delivery_corp (택배회사) 값을 입력해주세요. | delivery_corp 를 채워 보내요 |
RC_ESCROW_SHIPPING_NO_METHOD (2702) |
선택된 결제의 PG는 배송시작 API를 지원하지 않습니다. | 에스크로 배송정보 전송은 이니시스·KCP만 지원해요 (HTTP 404) |
RC_ESCROW_SHIPPING_START_FAILED (2701) |
PG가 내려준 실패 메시지 원문 (이니시스 resultMsg / KCP res_msg) |
HTTP 500 으로 내려와요. pg_error_code 에 PG 응답 코드가, message 에 PG 메시지가 그대로 담기니 이 값으로 원인을 확인하고 택배사명·송장번호·배송지 형식을 점검해요 |
API_ONLY_SELLER (600) |
판매점 계정만 사용할 수 있는 API입니다. | 리셀러/파트너 키가 아니라 셀러 프로젝트 키로 호출해요 |
