리셀러가 자기 화면에서 판매자의 사업자 정보·서류·약관 동의를 받아, 리셀러 서버가 하위 가맹점의 PG 가맹 신청서를 직접 낸다. 부트페이 신청 화면을 띄우지 않는 길이다. 부트페이 화면으로 받고 싶다면 등록 폼 세션 발급 을 쓴다.
핵심 요약
- 순서는 약관 조회 → (서류가 있으면) 업로드 허가 받기 → storage 에 서류 올리기 → 신청서 접수 다. 진행 상태는 등록 상태 조회 로 읽는다.
- 신청서와 약관 동의 기록은 한 번에 저장된다. 동의가 빠지거나 버전이 다르면 신청서도 남지 않는다.
- 약관 동의는 리셀러 화면에서 받으므로, 약관마다 동의 시각·동의한 사람의 IP·동의자를 함께 보낸다. 이 값이 그대로 동의 기록이 된다.
- 접수 응답은 등록 상태 조회의 한 항목과 같은 모양이다(
channel은api). - 지금 낼 수 있는 신청은 PG 가맹 신청(
pg_join) 하나다. A형(대표가맹점) 리셀러는 아직 쓸 수 없다.
이 API 로 신청서를 내면 "접수" 리셀러 웹훅(seller.onboarding.changed)과 리셀러 담당자 알림톡이 나가지 않는다. 접수 결과는 이 API 의 응답으로 바로 확인한다. 그 뒤 부트페이가 심사 상태를 바꾸면(서류 요청·반려·승인 등) 그때부터는 다른 입구와 똑같이 웹훅·알림톡이 간다.
리셀러 계정 키(Client Key·Secret Key)로 부르므로 리셀러 서버에서만 호출한다. 사업자등록증·정산 계좌·담당자 연락처를 리셀러가 직접 받는 길이라, 영업대행계약의 개인정보 처리 위탁 조항을 지키고 요청 본문을 로그·에러 리포팅에 남기지 않는다.
전체 흐름
| 단계 | 호출 | 받는 것 |
|---|---|---|
| 1. 약관 조회 | GET /v2/reseller/agreements |
지금 동의받아야 할 약관의 code·version·원문 |
| 2. 업로드 허가 | POST /v2/reseller/seller/:provider_id/files/grant |
10분짜리 grant·upload_url |
| 3. 서류 올리기 | POST {upload_url} (storage, 헤더 X-Upload-Grant) |
file_id 와 파일 정보 |
| 4. 접수 | POST /v2/reseller/seller/:provider_id/onboarding |
신청서 상태(pg_wait_id·status·stage·due_at …) |
| 5. 보완·재제출 | PUT /v2/reseller/seller/:provider_id/onboarding/:onboarding_id |
신청서 상태 |
1약관 조회
판매자에게 보여 줄 약관의 현재 버전과 원문을 읽는다. 접수 때 agreements[] 에는 여기서 받은 code·version 을 그대로 넣는다. 약관이 바뀌면 이전 버전으로 낸 접수는 거부된다.
https://api.bootpay.co.kr/v2/reseller/agreementsBasic Auth (리셀러 계정 키)| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
codes |
String | 선택 | 읽을 약관 코드를 쉼표로 (pg_terms,privacy_third_party). 비우면 전부 |
[
{
"code": "pg_terms",
"version": 2,
"title": "PG 서비스 이용약관",
"body": "(약관 원문)",
"effective_at": "2026-10-01T00:00:00+09:00",
"hash": "sha256:example_hash",
"required": true
}
]json| 파라미터 | 타입 | 설명 |
|---|---|---|
| code | String | 약관 코드 — pg_terms PG 서비스 이용약관 · privacy_third_party 개인정보 제3자 제공 동의 |
| version | Number | 지금 동의받아야 하는 버전 |
| title · body | String | 화면에 보여 줄 제목·원문(마크다운) |
| effective_at | String | 이 버전의 시행 시각(ISO 8601) |
| hash | String | 원문 해시. 동의 기록에 함께 남는다 |
| required | Boolean | 빠지면 접수가 거부되는 필수 약관인지 |
2서류 업로드 허가
사업자등록증을 올릴 수 있는 10분짜리 허가를 받는다. 파일은 부트페이 API 서버를 거치지 않고 리셀러 서버가 upload_url 로 직접 올린다.
https://api.bootpay.co.kr/v2/reseller/seller/:provider_id/files/grantBasic Auth (리셀러 계정 키)| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
provider_id |
String | 필수 | 하위 가맹점의 provider_id (URL 경로) |
kind |
String | 필수 | 서류 종류. 지금은 business_license(사업자등록증)만 |
app_id |
String | 선택 | 서류를 둘 하위 프로젝트. 비우면 그 가맹점에서 가장 먼저 만든 프로젝트 |
{
"grant": "eyJ2IjoxLCJwX2lkIjoi...example.0f1e2d3c...",
"upload_url": "https://s.bootapi.com/api/v1/secure_documents",
"expires_at": "2026-10-27T15:10:00+09:00"
}json받은 grant 를 X-Upload-Grant 헤더에 넣고, upload_url 로 multipart 업로드한다. 파일은 PDF·JPG·PNG, 10MB 이하다.
| 필드 | 값 |
|---|---|
헤더 X-Upload-Grant |
받은 grant 그대로 |
doc_type |
business_license (허가의 kind 와 같아야 한다) |
file |
서류 파일 |
storage 응답의 file_id 를 접수의 file_ids[] 에 넣는다. 접수할 때 부트페이가 그 파일이 이 하위 가맹점이 올린 사업자등록증인지 확인하고, 파일 이름·형식·크기는 저장된 값으로 신청서에 남긴다. 다른 가맹점이 올린 파일 id 는 받지 않는다. 나머지 값(file_name·ext·mime_type·size)은 business_license 로 함께 보내도 되지만 저장된 값이 우선한다.
{
"file_id": "6718a0aa8f1b5b00367a0201",
"file_name": "사업자등록증.pdf",
"ext": "pdf",
"mime_type": "application/pdf",
"size": 182044
}json허가 하나로 여러 번 올릴 수 있지만 10분이 지나면 다시 받는다. 하위 가맹점의 커머스 연동이 아직 없으면 storage 가 STORAGE_SECURE_DOC_GRANT_OWNER_NOT_FOUND(3037)로 거부한다 — 커머스 연동 재시도 뒤 다시 올린다.
3신청서 접수
https://api.bootpay.co.kr/v2/reseller/seller/:provider_id/onboardingBasic Auth (리셀러 계정 키)| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
provider_id |
String | 필수 | 신청할 하위 가맹점의 provider_id (URL 경로) |
pg_alias |
String | 필수 | 신청할 PG 별칭 하나. 부트페이 추천(suggest)은 받지 않는다 |
agreements |
Array | 필수 | 약관 동의 — 아래 동의 항목 표. 필수 약관(required: true)마다 1개 |
file_ids |
Array | 선택 | 업로드한 사업자등록증 file_id (지금은 1개까지) |
business_license |
Object | 선택 | 업로드 응답의 file_name·ext·mime_type·size |
company_name |
String | 필수 | 상호 |
seller_type |
Number | 필수 | 사업자 구분 (3 개인사업자 · 4 법인사업자) |
regist_no |
String | 필수 | 사업자등록번호 |
corp_reg_no |
String | 필수 | 법인사업자는 법인등록번호, 개인사업자는 대표자 주민등록번호(숫자 13자리) |
owner_name |
String | 필수 | 대표자명 |
business_type · business_item |
String | 필수 | 업종 · 업태 |
biz_email · biz_tel |
String | 필수 | 회사 이메일 · 회사 전화번호 |
name · email · phone |
String | 필수 | 담당자 이름 · 이메일 · 휴대폰 |
site_url |
String | 필수 | 서비스 주소(본인인증 신청 type=4 는 선택) |
real_type |
Number | 선택 | 판매 상품 — 1 실물 · 0 비실물 · -1 둘 다. 비우면 1 |
그 밖의 칸 |
— | 선택 | service_name zip addr1 addr2 biz_fax tel tax_type good_type desc type pay_method max_term month_price good_price policy test_user_id test_user_password bank_code account_number account_owner expect_mid owner_birth_date corp_capital — 부트페이 콘솔 신청서와 같은 칸·같은 검사 |
라이트페이(lightpay)는 희망 MID(expect_mid, 영문·숫자 10자 이내)와 정산 계좌(bank_code·account_number·account_owner)가 필수이고, 법인이면 대표자 생년월일(owner_birth_date, YYYYMMDD)·자본금(corp_capital)도 필수다. 글자 칸에는 문자열이나 숫자만 보낸다.
응답을 못 받아 같은 접수를 다시 보낼 때 신청서가 두 건 생기지 않게 하려면, 접수마다 새 값(예: UUID)을 Idempotency-Key 헤더에 넣는다. 같은 키로 다시 보내면 새로 만들지 않고 처음 만든 신청서를 같은 응답 모양으로 돌려준다(약관 동의 기록도 다시 남기지 않는다). 키는 64자 이내 영문·숫자·- _ : . 이고, 다른 가맹점 접수에 쓴 키를 다시 쓰면 IDEMPOTENCY_KEY_REUSED 다. 헤더를 빼면 지금처럼 보낼 때마다 새 신청서다.
동의 항목 (agreements[] 한 개)
| 필드 | 필수 | 설명 |
|---|---|---|
| code | Y | 약관 코드 (1. 약관 조회의 code) |
| version | Y | 판매자에게 보여 준 버전 (1. 약관 조회의 version 과 같아야 한다) |
| agreed_at | Y | 판매자가 동의한 시각, ISO 8601(시간대 포함 권장). 지금보다 5분 넘게 뒤면 거부 |
| ip | Y | 동의한 판매자의 IP(IPv4·IPv6 한 개). 리셀러 서버 IP 가 아니다 |
| signer.name | N | 동의자 이름(50자 이하). 비우면 신청서 담당자 이름 |
| signer.phone | N | 동의자 휴대폰(숫자, - 허용). 비우면 신청서 담당자 휴대폰. 부트페이는 암호화해 보관한다 |
코드 예제
import fetch from 'node-fetch'
import fs from 'node:fs'
const RESELLER_AUTH = 'Basic ' + Buffer
.from(`${process.env.BOOTPAY_RESELLER_CLIENT_KEY}:${process.env.BOOTPAY_RESELLER_SECRET_KEY}`)
.toString('base64')
const API = 'https://api.bootpay.co.kr/v2/reseller'
const providerId = '65a1c0aa8f1b5b00367a0001'
// 1. 지금 동의받을 약관
const terms = await (await fetch(`${API}/agreements`, { headers: { Authorization: RESELLER_AUTH } })).json()
// 2·3. 업로드 허가 → storage 에 직접 올리기
const grant = await (await fetch(`${API}/seller/${providerId}/files/grant`, {
method: 'POST',
headers: { Authorization: RESELLER_AUTH, 'Content-Type': 'application/json' },
body: JSON.stringify({ kind: 'business_license' })
})).json()
const form = new FormData()
form.append('doc_type', 'business_license')
form.append('file', new Blob([fs.readFileSync('./license.pdf')], { type: 'application/pdf' }), 'license.pdf')
const file = await (await fetch(grant.upload_url, {
method: 'POST',
headers: { 'X-Upload-Grant': grant.grant },
body: form
})).json()
// 4. 접수 — 판매자가 리셀러 화면에서 동의한 시각·IP 를 그대로 보낸다
const consent = { agreed_at: '2026-10-27T10:15:00+09:00', ip: '198.51.100.7', signer: { name: '김대표', phone: '01011112222' } }
const submitKey = crypto.randomUUID() // 이 접수를 다시 보낼 때도 같은 값을 쓴다
const res = await fetch(`${API}/seller/${providerId}/onboarding`, {
method: 'POST',
headers: { Authorization: RESELLER_AUTH, 'Content-Type': 'application/json', 'Idempotency-Key': submitKey },
body: JSON.stringify({
pg_alias: 'nicepay',
company_name: '예시상점', seller_type: 4, regist_no: '1234567890', corp_reg_no: '1101111234567',
owner_name: '김대표', business_type: '서비스', business_item: '소프트웨어',
biz_email: 'biz@example.com', biz_tel: '0212345678', site_url: 'https://shop.example.com',
name: '이담당', email: 'manager@example.com', phone: '01033334444',
file_ids: [file.file_id],
business_license: { file_name: file.file_name, ext: file.ext, mime_type: file.mime_type, size: file.size },
agreements: terms.map((t) => ({ code: t.code, version: t.version, ...consent }))
})
})
const onboarding = await res.json() // { pg_wait_id, status, stage, due_at, ... }javascriptimport base64, os, requests
auth = 'Basic ' + base64.b64encode(
f"{os.environ['BOOTPAY_RESELLER_CLIENT_KEY']}:{os.environ['BOOTPAY_RESELLER_SECRET_KEY']}".encode()
).decode()
api = 'https://api.bootpay.co.kr/v2/reseller'
provider_id = '65a1c0aa8f1b5b00367a0001'
terms = requests.get(f'{api}/agreements', headers={'Authorization': auth}).json()
grant = requests.post(f'{api}/seller/{provider_id}/files/grant', headers={'Authorization': auth},
json={'kind': 'business_license'}).json()
with open('license.pdf', 'rb') as f:
file = requests.post(grant['upload_url'], headers={'X-Upload-Grant': grant['grant']},
data={'doc_type': 'business_license'},
files={'file': ('license.pdf', f, 'application/pdf')}).json()
consent = {'agreed_at': '2026-10-27T10:15:00+09:00', 'ip': '198.51.100.7'}
res = requests.post(f'{api}/seller/{provider_id}/onboarding', headers={'Authorization': auth}, json={
'pg_alias': 'nicepay',
'company_name': '예시상점', 'seller_type': 4, 'regist_no': '1234567890', 'corp_reg_no': '1101111234567',
'owner_name': '김대표', 'business_type': '서비스', 'business_item': '소프트웨어',
'biz_email': 'biz@example.com', 'biz_tel': '0212345678', 'site_url': 'https://shop.example.com',
'name': '이담당', 'email': 'manager@example.com', 'phone': '01033334444',
'file_ids': [file['file_id']],
'agreements': [{'code': t['code'], 'version': t['version'], **consent} for t in terms]
})
onboarding = res.json()pythonRESELLER_AUTH=$(printf '%s' "$BOOTPAY_RESELLER_CLIENT_KEY:$BOOTPAY_RESELLER_SECRET_KEY" | base64)
PROVIDER_ID='65a1c0aa8f1b5b00367a0001'
# 2. 업로드 허가
curl -X POST "https://api.bootpay.co.kr/v2/reseller/seller/$PROVIDER_ID/files/grant" \
-H "Authorization: Basic $RESELLER_AUTH" -H 'Content-Type: application/json' \
-d '{"kind":"business_license"}'
# 3. storage 에 올리기 — 위 응답의 grant·upload_url
curl -X POST "$UPLOAD_URL" -H "X-Upload-Grant: $UPLOAD_GRANT" \
-F doc_type=business_license -F file=@license.pdf
# 4. 접수
curl -X POST "https://api.bootpay.co.kr/v2/reseller/seller/$PROVIDER_ID/onboarding" \
-H "Authorization: Basic $RESELLER_AUTH" -H 'Content-Type: application/json' \
-d @onboarding.jsonbash응답
접수·보완 응답은 등록 상태 조회 목록의 한 항목과 같다.
{
"provider_id": "65a1c0aa8f1b5b00367a0001",
"onboarding_id": "6718a0aa8f1b5b00367a0101",
"kind": "pg_join",
"status": 0,
"status_name": "미확인",
"stage": "intake",
"stage_owner": "ours",
"due_at": "2026-10-28T10:20:00+09:00",
"reject_reason": null,
"pg_wait_id": "6718a0aa8f1b5b00367a0101",
"pg": "nicepay",
"overdue": false,
"required_documents": [],
"channel": "api",
"c_at": "2026-10-27T10:20:00+09:00",
"u_at": "2026-10-27T10:20:00+09:00"
}json| 파라미터 | 타입 | 설명 |
|---|---|---|
| pg_wait_id · onboarding_id | String | 신청서 식별자. 보완·재제출과 상태 조회에 쓴다 |
| status · status_name | Number · String | 신청 상태 번호·이름. 접수 직후는 0 |
| stage · stage_owner | String | 단계와 지금 할 차례 — 등록 상태 조회 의 단계 표 |
| due_at | String | 이 단계의 기한(ISO 8601) |
| channel | String | 접수 입구. 이 API 로 낸 신청서는 api |
| 그 밖의 키 | — | 등록 상태 조회와 같다 |
4보완·재제출
서류 보완 요청을 받았거나 내용을 고칠 때 같은 신청서를 다시 낸다. 판매자가 부트페이 콘솔이나 등록 폼으로 낸 그 가맹점의 신청서도 고칠 수 있다.
https://api.bootpay.co.kr/v2/reseller/seller/:provider_id/onboarding/:onboarding_idBasic Auth (리셀러 계정 키)| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
provider_id |
String | 필수 | 하위 가맹점의 provider_id (URL 경로) |
onboarding_id |
String | 필수 | 고칠 신청서 (URL 경로, 접수 응답의 pg_wait_id) |
agreements |
Array | 필수 | 약관 동의 — 다시 내는 것이라 동의도 다시 받는다(접수와 같은 모양) |
pg_alias |
String | 선택 | 바꿀 PG. 비우면 신청서의 지금 PG |
file_ids |
Array | 선택 | 바꿀 사업자등록증 file_id. 비우거나 빈 배열이면 그대로 |
remove_business_license |
Boolean | 선택 | true 면 첨부한 사업자등록증을 뺀다 |
신청서 칸 |
— | 선택 | 접수와 같은 칸. 보낸 칸만 바뀐다 — real_type 도 안 보내면 그대로 |
신청이 끝난(status 6) 신청서는 고칠 수 없다. 새로 신청하려면 3. 신청서 접수로 다시 낸다. 응답은 접수와 같다.
에러 코드
| 코드 | 메시지 | 대처 방법 |
|---|---|---|
API_ONLY_RESELLER (601) |
리셀러만 이용이 가능한 API 다 | 리셀러 권한 계정 키로 호출한다 |
API_SCOPE_INVALID (1222) |
허용되지 않은 API 권한의 키다 (HTTP 401) | 키에 이 API 의 리셀러 스코프 를 주거나 전체 권한(supervisor) 키로 호출한다 |
RESELLER_SUSPENDED (343) |
본사가 리셀러 사용을 멈췄다 (HTTP 403) | 부트페이에 문의한다 |
ONBOARDING_KIND_TYPE_MISMATCH (8168) |
이 리셀러 유형으로는 할 수 없는 신청이다 (HTTP 403). PG 가맹 신청은 B형(영업대행) 리셀러만 할 수 있다 — 메시지에 이 계정의 유형이 나온다 | 지금 유형은 리셀러 내 정보 의 reseller_contract.type 으로 확인한다. A형(대표가맹점)용 신청은 아직 열리지 않았다. 유형을 바꾸려면 부트페이에 문의한다 |
PROVIDER_NOT_FOUND (300) |
본인 하위 가맹점이 아니다 (HTTP 404) | provider_id 가 본인 하위 가맹점인지 확인한다 |
PG_WAIT_NOT_FOUND (11000) |
그 가맹점의 신청서가 아니다 (HTTP 404) | onboarding_id 를 등록 상태 조회로 다시 확인한다 |
PW_NOT_EDITABLE_STATUS (2232) |
신청이 끝난 건은 고칠 수 없다 | 새 신청은 접수 API 로 낸다 |
PG_NOT_FOUND (1000) |
없거나 쓰지 않는 PG, 또는 suggest 다 (HTTP 404) |
PG 코드 에서 별칭을 확인한다 |
APP_NOT_FOUND (200) |
그 가맹점의 사용 중인 프로젝트가 아니다 (HTTP 404) | 업로드 허가의 app_id 를 확인하거나 비운다 |
AGREEMENT_TERMS_NOT_READY (8120) |
동의받을 약관이 아직 준비되지 않았다 | 부트페이에 문의한다 |
AGREEMENT_VERSION_MISMATCH (8121) |
약관 버전이 지금 버전과 다르다 (payload.codes) |
약관을 다시 조회해 바뀐 원문으로 동의를 다시 받는다 |
AGREEMENT_REQUIRED_MISSING (8122) |
필수 약관 동의가 빠졌다 (payload.codes) |
빠진 약관 동의를 받아 넣는다 |
AGREEMENT_EVIDENCE_INVALID (8170) |
동의 시각·IP·동의자 값이 올바르지 않다 (payload.codes) |
agreed_at(ISO 8601, 미래 아님)·ip(한 개 주소)·signer 를 확인한다 |
PW_FILE_IDS_INVALID (8171) |
file_ids 가 2개 이상이거나 파일 id 모양이 아니다. 또는 이 하위 가맹점이 올린 사업자등록증이 아니다(없거나 지운 파일 포함) |
이 가맹점의 업로드 허가로 올린 응답의 file_id 하나만 보낸다 |
COMMERCE_REQUEST_FAILED (8182) |
서류 주인을 확인하지 못했다(연동 서버 오류). 신청서는 저장되지 않는다 | 잠시 뒤 다시 보낸다 |
IDEMPOTENCY_KEY_REUSED (8210) |
이 Idempotency-Key 는 다른 가맹점의 접수에 이미 썼다 |
접수마다 새 키를 만든다 |
PARAMS_INVALID (-33) |
약관 코드·버전 모양이 틀리거나 글자 칸에 객체·배열이 들어왔다. 업로드 허가의 kind 가 틀렸다 |
메시지의 칸을 고친다 |
PW_* (2201~2230) |
신청서 필수 칸이 비었거나 형식이 틀렸다 | 메시지의 칸을 채운다(부트페이 콘솔 신청서와 같은 검사) |
STORAGE_SECURE_DOC_GRANT_INVALID (3036) |
storage 업로드 — 허가가 틀렸다 (HTTP 401) | 받은 grant 를 고치지 않고 그대로 넣는다 |
STORAGE_SECURE_DOC_GRANT_EXPIRED (3038) |
storage 업로드 — 허가 10분이 지났다 (HTTP 401) | 업로드 허가를 다시 받는다 |
STORAGE_SECURE_DOC_GRANT_OWNER_NOT_FOUND (3037) |
storage 업로드 — 하위 가맹점의 커머스 연동이 없다 | 커머스 연동 재시도 뒤 다시 올린다 |


