마켓플레이스

PG 가맹 신청

아직 MID 가 없는 하위 판매자의 PG 가맹 신청을 받습니다. 화면이 필요하면 등록 폼을, 이미 받은 정보로 낼 때는 등록 API 를 사용합니다. B형(영업대행) 리셀러만 쓸 수 있습니다 — 리셀러 가입·유형.

API 하는 일
1. 등록 폼 세션 발급 등록 폼 토큰 발급
2. 등록 폼 열기 (JS SDK) 리셀러 화면에서 등록 폼 열기
3. 등록 상태 조회 하위 가맹점의 PG 신청 진행 상태
4. 등록 API 접수 화면 없이 신청서 접수·보완, 약관 조회, 서류 업로드 허가
5. 신청서 서류 열기 신청서 서류 목록과 3분 열람 주소

인증·권한 오류는 각 표에 없어도 공통 오류 가 날 수 있습니다.

1등록 폼 세션 발급

하위 가맹점 한 곳의 PG 가맹 신청 화면(등록 폼)을 열 수 있는 토큰을 발급합니다. 받은 token 을 등록 폼 열기 (JS SDK) 에 넘기거나 url 을 그대로 열면, 판매자는 부트페이 계정 없이 신청서를 작성해 접수합니다.

동작 규칙

  • 토큰은 하위 가맹점 1곳 · 30분 · 접수 1회​​로 묶입니다. 접수하면 소진되고, 30분이 지나면 열리지 않습니다.
  • 토큰 평문은 이 응답에서만 나옵니다. 부트페이는 해시만 저장하므로 다시 조회할 수 없습니다. 필요하면 새로 발급합니다.
  • 같은 하위 가맹점·같은 종류로 다시 발급하면 아직 쓰지 않은 이전 토큰은 바로 만료됩니다.
  • 지금 열 수 있는 종류(kind)는 pg_join(PG 가맹 신청) 하나입니다.
  • 신청 단계가 바뀔 때마다 리셀러 웹훅 이 오고, 놓친 단계는 등록 상태 조회 로 다시 맞춥니다.
서버에서만 발급합니다

이 API 는 리셀러 계정 키(Client Key·Secret Key)로 부릅니다. 브라우저·앱에 리셀러 키를 두지 말고, 리셀러 서버에서 발급한 token 만 판매자 화면에 넘깁니다. 토큰을 로그에 남기면 30분 동안 누구나 그 가맹점 이름으로 신청서를 낼 수 있습니다.

API 정보

POSThttps://api.bootpay.co.kr/v2/reseller/seller/:provider_id/onboarding/sessionBasic Auth (리셀러 계정 키)

1-1. 요청 파라미터

파라미터 타입 필수 설명
provider_id String 필수 신청을 받을 하위 가맹점의 provider_id (URL 경로)
kind String 필수 등록 폼 종류. 지금은 pg_join 만
mode String 선택 new 새 신청(기본) · edit 기존 신청서 수정·재제출
pg_wait_id String 선택 mode=edit 일 때 필수. 고칠 신청서의 onboarding_id
allowed_pgs Array 선택 신청할 PG 별칭. new 는 정확히 1개 필수, edit 는 비우면 신청서의 PG
allowed_methods Array 선택 고를 수 있는 결제수단 별칭. 비우면 그 PG 의 결제수단 전부
app_id String 선택 PG 승인 뒤 MID 를 연결할 하위 프로젝트. 비우면 그 가맹점에서 가장 먼저 만든 프로젝트
prefill Object 선택 신청서에 미리 채울 값 ({키: 값}, 값은 문자열·숫자, 1000자 이하)
locked Array 선택 판매자가 고칠 수 없게 잠글 키. prefill 에 있는 키만
return_url String 선택 접수 뒤 돌아갈 주소. https:// 만, 2048자 이하
brand Object 선택 신청 화면 상단 브랜드 {name(30자 이하), logo_url(https)}

prefill·locked 에 쓸 수 있는 키는 아래와 같습니다. 이 밖의 키를 보내면 400 ONBOARDING_PREFILL_INVALID 로 거부됩니다(오타를 바로 알 수 있게 조용히 버리지 않습니다). pg_alias 를 보내면 allowed_pgs 의 PG 와 같아야 합니다.

묶음 키
사업자 company_name seller_type regist_no corp_reg_no owner_name owner_birth_date corp_capital zip addr1 addr2 biz_email biz_tel biz_fax tax_type
서비스 service_name site_url business_type business_item real_type good_type desc max_term month_price good_price policy type
담당자 name email phone tel
정산 계좌 bank_code account_number account_owner
PG pg_alias pay_method expect_mid

1-2. 코드 예제

import fetch from 'node-fetch'

const RESELLER_AUTH = 'Basic ' + Buffer
  .from(`${process.env.BOOTPAY_RESELLER_CLIENT_KEY}:${process.env.BOOTPAY_RESELLER_SECRET_KEY}`)
  .toString('base64')

const providerId = '65a1c0aa8f1b5b00367a0001'
const res = await fetch(`https://api.bootpay.co.kr/v2/reseller/seller/${providerId}/onboarding/session`, {
    method: 'POST',
    headers: {
        'Authorization': RESELLER_AUTH,
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        kind: 'pg_join',
        allowed_pgs: ['nicepay'],
        prefill: { company_name: '예시상점', regist_no: '1234567890' },
        locked: ['company_name', 'regist_no'],
        return_url: 'https://reseller.example.com/onboarding/done',
        brand: { name: '예시 리셀러', logo_url: 'https://reseller.example.com/logo.png' }
    })
})
const { token, url, expires_at } = await res.json()
// token 만 판매자 화면에 넘긴다 — 로그에 남기지 않는다javascript

1-3. 응답

{
  "token":      "rst_example_token",
  "url":        "https://admin.bootpay.co.kr/onboarding/s/rst_example_token",
  "expires_at": "2026-10-27T15:30:00+09:00"
}json

응답 파라미터

파라미터 타입 설명
token String 등록 폼 토큰(rst_ 로 시작). 이 응답에서만 나옵니다
url String 토큰으로 바로 여는 신청 화면 주소. SDK 없이 새 창·리다이렉트로 열 때 사용합니다
expires_at String 만료 시각(ISO 8601, 발급 + 30분)

1-4. 판매자가 열었을 때

상황 신청 화면
30분이 지남 "신청 주소를 쓸 수 있는 시간이 지났습니다" — 새로 발급합니다
이미 접수함 "이미 접수한 신청 주소입니다" — 고칠 내용은 mode=edit 로 새로 발급합니다
같은 가맹점에 새 토큰을 발급함 이전 토큰은 만료로 안내됩니다
리셀러 사용이 멈춤 열리지 않습니다

1-5. 에러 코드

코드 메시지 대처 방법
API_ONLY_RESELLER (601) 리셀러만 이용이 가능한 API입니다 리셀러 권한 계정 키로 호출합니다
API_SCOPE_INVALID 허용되지 않은 API 권한의 키입니다 (HTTP 401) 키에 이 API 의 리셀러 스코프 를 주거나 전체 권한(supervisor) 키로 호출합니다
RESELLER_SUSPENDED (343) 본사가 리셀러 사용을 멈췄습니다 (HTTP 403) 부트페이에 문의합니다
ONBOARDING_KIND_INVALID kind 가 없거나 모르는 값입니다 pg_join 으로 보냅니다
ONBOARDING_KIND_NOT_READY 아직 열지 않은 종류입니다(sub_mall) 지금은 pg_join 만 사용합니다
ONBOARDING_KIND_TYPE_MISMATCH (8168) 이 리셀러 유형으로는 만들 수 없는 등록 폼입니다 (HTTP 403). PG 가맹 신청(pg_join)은 B형(영업대행) 리셀러만 만들 수 있습니다 — 메시지에 이 계정의 유형이 나옵니다 지금 유형은 리셀러 내 정보 의 reseller_contract.type 으로 확인합니다. 바꾸려면 부트페이에 문의합니다
PROVIDER_NOT_FOUND (300) 본인 하위 가맹점이 아닙니다 (HTTP 404) provider_id 가 본인 하위 가맹점인지 확인합니다
ONBOARDING_MODE_INVALID mode 가 new·edit 가 아닙니다 new 또는 edit 로 보냅니다
PG_WAIT_NOT_FOUND (11000) 그 가맹점의 신청서가 아닙니다 (HTTP 404) pg_wait_id 를 등록 상태 조회 의 onboarding_id 로 보냅니다
PW_NOT_EDITABLE_STATUS (2232) 신청이 끝난 건은 고칠 수 없습니다 새 신청은 mode=new 로 발급합니다
APP_NOT_FOUND (200) 그 가맹점의 사용 중인 프로젝트가 아닙니다 (HTTP 404) app_id 를 확인하거나 비웁니다
ONBOARDING_ALLOWED_PGS_INVALID allowed_pgs 가 1개가 아니거나 신청서의 PG 와 다릅니다 PG 별칭을 하나만 보냅니다
PG_NOT_FOUND 없거나 쓰지 않는 PG입니다 (HTTP 404) PG 코드 에서 별칭을 확인합니다
ONBOARDING_ALLOWED_METHODS_INVALID 그 PG 에서 쓸 수 없는 결제수단이 있습니다 결제수단 별칭을 확인하거나 비웁니다
ONBOARDING_PREFILL_INVALID prefill·locked 에 받을 수 없는 키나 값이 있습니다 위 허용 키 표를 확인합니다
ONBOARDING_RETURN_URL_INVALID return_url 이 https 가 아니거나 너무 깁니다 2048자 이하의 https:// 주소로 보냅니다
ONBOARDING_BRAND_INVALID brand 값이 올바르지 않습니다 name 30자 이하, logo_url 은 https://

2등록 폼 열기 (JS SDK)

리셀러 서버가 등록 폼 세션 발급 으로 받은 token 을 판매자 화면에 넘기고, Bootpay.requestOnboarding({ token }) 으로 부트페이 PG 가맹 신청 화면을 엽니다. 판매자가 접수하면 Promise 가 done 으로 끝납니다.

동작 규칙

  • 결제창과 같은 JS SDK(@bootpay/client-js)의 함수입니다. 웹 브라우저에서만 열립니다.
  • 여는 방식(display)은 iframe(기본, 지금 페이지 위 덮개) · popup(새 창) · redirect(지금 창 이동) 셋입니다.
  • 접수하면 resolve {event: 'done', id, kind, pg_wait_id, status}, 판매자가 닫거나 「나중에 할게요」를 누르면 reject {event: 'cancel'}입니다.
  • 화면 결과는 표시용입니다. 최종 상태는 리셀러 서버가 리셀러 웹훅 이나 등록 상태 조회 로 확인합니다.
  • 토큰은 리셀러 서버에서 발급합니다. 리셀러 키를 브라우저에 두지 않습니다.

2-1. 함수

Bootpay.requestOnboarding(data: {
    token:    string                              // 등록 폼 토큰(rst_…) — 필수
    display?: 'iframe' | 'popup' | 'redirect'      // 기본 iframe
    url?:     string                              // 세션 발급 응답의 url 을 그대로 쓸 때만(부트페이 신청 화면 주소만 받는다)
}): Promise<{
    event:       'done' | 'cancel' | 'error'
    id?:         string        // 신청서 식별자(= onboarding_id)
    kind?:       string        // 'pg_join'
    pg_wait_id?: string        // id 와 같은 값
    status?:     number | null // 접수 직후 신청 상태
    error_code?: string
    message?:    string
}>typescript

2-2. 코드 예제

import { Bootpay } from '@bootpay/client-js'

// 리셀러 서버가 등록 폼 세션 발급 API 로 받은 token 만 화면에 넘긴다
const { token } = await fetch('/api/seller/onboarding-session', { method: 'POST' }).then((r) => r.json())

try {
    const result = await Bootpay.requestOnboarding({ token, display: 'iframe' })
    // result.event === 'done'
    showMessage('PG 신청을 접수했어요. 심사 진행은 알림으로 알려 드릴게요.')
    console.log(result.pg_wait_id, result.status)
} catch (e) {
    if (e.event === 'cancel') {
        // 판매자가 닫았다 — 나중에 다시 열 수 있게 둔다
    } else {
        console.error(e.error_code, e.message)
    }
}javascript

2-3. 여는 방식

display 열리는 모양 끝날 때
iframe (기본) 지금 페이지 위에 덮개로 엽니다 접수하면 done, 덮개를 닫으면 cancel
popup 새 창으로 엽니다. 버튼 클릭 같은 사용자 동작 안에서 불러야 팝업이 막히지 않습니다 접수하면 done, 창을 닫으면 cancel. 팝업이 막히면 POPUP_BLOCKED
redirect 지금 창을 신청 화면으로 옮깁니다. Promise 는 끝나지 않습니다 세션의 return_url 로 돌아옵니다(아래)

redirect 로 돌아올 때

세션을 발급할 때 return_url 을 넣으면, 접수 뒤 그 주소에 결과 쿼리를 붙여 돌려보냅니다. 토큰은 쿼리에 넣지 않습니다.

https://reseller.example.com/onboarding/done?event=done&id=6718a0aa8f1b5b00367a0101&kind=pg_join&pg_wait_id=6718a0aa8f1b5b00367a0101&status=0
https://reseller.example.com/onboarding/done?event=cancel

쿼리 값은 누구나 바꿔 붙일 수 있습니다. 화면 안내에만 쓰고, 상태는 서버에서 등록 상태 조회 로 다시 확인합니다.

앱(웹뷰)에서 열 때

앱용 SDK 에는 아직 이 함수가 없습니다. 세션 발급 응답의 url 을 웹뷰나 외부 브라우저로 열고, return_url 을 앱이 받을 수 있는 https 주소로 넣어 돌아오는 시점을 잡습니다.

2-4. 신청 화면이 열리지 않을 때

상황 화면·결과 할 일
30분이 지난 토큰 "신청 주소를 쓸 수 있는 시간이 지났습니다" 서버에서 새로 발급합니다
이미 접수한 토큰 "이미 접수한 신청 주소입니다" 고칠 내용이 있으면 mode=edit 로 새로 발급합니다
같은 가맹점에 새 토큰을 발급함 이전 토큰은 만료로 안내됩니다 마지막에 받은 토큰을 사용합니다
리셀러 사용이 멈춤 열리지 않습니다 부트페이에 문의합니다

2-5. 에러 코드

코드 메시지 대처 방법
ONBOARDING_TOKEN_REQUIRED token 이 없습니다 세션 발급 응답의 token 을 넘깁니다
ONBOARDING_URL_INVALID url 이 부트페이 신청 화면 주소가 아닙니다 세션 발급 응답의 url 을 그대로 넘기거나 url 을 빼고 token 만 넘깁니다
POPUP_BLOCKED 브라우저가 팝업을 막았습니다 버튼 클릭 안에서 부르거나 display: 'iframe' 을 사용합니다
ONBOARDING_BROWSER_ONLY 브라우저가 아닌 곳(서버 렌더링 등)에서 불렀습니다 브라우저에서 부릅니다

3등록 상태 조회

하위 가맹점 한 곳의 PG 가맹 신청서를 최신순으로 읽습니다. 각 항목의 값은 리셀러 웹훅 의 data 와 같은 이름이라, 웹훅을 놓쳤거나 순서가 뒤바뀌었을 때 이 API 로 최신 상태를 다시 맞춥니다.

동작 규칙

  • 그 가맹점의 신청서를 전부 보여 줍니다. 리셀러가 발급한 등록 폼으로 낸 신청(channel=hosted)뿐 아니라 판매자가 부트페이 콘솔에서 직접 낸 신청(console)도 나옵니다.
  • 최신순 20건까지. onboarding_id 를 주면 그 1건만 옵니다.
  • 신청이 없으면 {"list": []} 로 200입니다.
  • 본사 담당자의 메모·연락 기록·제출 서류·담당자 연락처는 내려 주지 않습니다.

API 정보

GEThttps://api.bootpay.co.kr/v2/reseller/seller/:provider_id/onboardingBasic Auth (리셀러 계정 키)

3-1. 요청 파라미터

파라미터 타입 필수 설명
provider_id String 필수 하위 가맹점의 provider_id (URL 경로)
onboarding_id String 선택 신청서 하나만 볼 때 (쿼리)

3-2. 코드 예제

import fetch from 'node-fetch'

const RESELLER_AUTH = 'Basic ' + Buffer
  .from(`${process.env.BOOTPAY_RESELLER_CLIENT_KEY}:${process.env.BOOTPAY_RESELLER_SECRET_KEY}`)
  .toString('base64')

const providerId = '65a1c0aa8f1b5b00367a0001'
const res = await fetch(`https://api.bootpay.co.kr/v2/reseller/seller/${providerId}/onboarding`, {
    headers: { 'Authorization': RESELLER_AUTH }
})
const { list } = await res.json()
console.log(list[0]?.stage, list[0]?.stage_owner)javascript

3-3. 응답

{
  "list": [
    {
      "provider_id":        "65a1c0aa8f1b5b00367a0001",
      "onboarding_id":      "6718a0aa8f1b5b00367a0101",
      "kind":               "pg_join",
      "status":             1,
      "status_name":        "구비서류 전달 필요",
      "stage":              "doc_wait",
      "stage_owner":        "merchant",
      "due_at":             "2026-10-30T18:00:00+09:00",
      "reject_reason":      null,
      "pg_wait_id":         "6718a0aa8f1b5b00367a0101",
      "pg":                 "nicepay",
      "overdue":            false,
      "required_documents": ["doc"],
      "channel":            "hosted",
      "c_at":               "2026-10-27T10:00:00+09:00",
      "u_at":               "2026-10-27T11:20:00+09:00"
    }
  ]
}json

응답 파라미터

파라미터 타입 설명
list Array 신청서 목록(최신순, 최대 20건)
list[].provider_id String 하위 가맹점 provider_id
list[].onboarding_id String 신청서 식별자. mode=edit 세션의 pg_wait_id 로 사용합니다
list[].pg_wait_id String onboarding_id 와 같은 값
list[].kind String pg_join
list[].status Number 신청 상태 번호
list[].status_name String 신청 상태 이름
list[].stage String 단계 — 아래 단계 표
list[].stage_owner String 지금 할 차례: ours 부트페이 · merchant 판매자 · pg PG사 · 끝난 단계는 null
list[].due_at String 이 단계의 기한(ISO 8601). 기한이 없는 단계는 null
list[].overdue Boolean 기한이 지났는지
list[].required_documents Array 서류 대기 단계일 때의 대기 사유 코드(doc 서류 · esign 전자계약 · fee 가맹비 · insurance 보증보험 · service_fix 서비스 보완 · review_fix PG 심사 보완 · card_fix 카드사 보완). 그 밖의 단계는 []
list[].reject_reason String 반려 사유(반려일 때만, 그 밖은 null)
list[].pg String 신청한 PG 별칭. 아직 정해지지 않았으면 null
list[].channel String 접수 입구: hosted 리셀러 등록 폼 · console 부트페이 콘솔 · api 리셀러 API
list[].c_at · u_at String 접수·마지막 변경 시각(ISO 8601)

3-4. 단계 표

stage 뜻 (status) stage_owner
intake 접수됨 — 부트페이가 확인 중 (0 미확인) ours
doc_wait 판매자의 서류·정보 보완을 기다림 (1 구비서류 전달 필요 · 3 미비사항 전달 필요 · 4 구비서류 원본 등기 필요) merchant
doc_review 부트페이가 서류 검토 중 (2 구비서류 확인중) ours
pg_submit PG사에 접수함 (10 PG사 접수) pg
pg_review PG사 심사 중 (11 PG사 접수 확인 · 5 PG사 심사 진행중 · 7 진행중) pg
done 신청 완료 (6 신청완료) merchant
closed 끝남 (8 심사반려 · 9 신청 철회) null

doc_wait 이면 판매자에게 보완을 안내합니다. 고친 신청서는 같은 onboarding_id 로 mode=edit 세션을 발급 해 다시 내게 합니다.

3-5. 에러 코드

코드 메시지 대처 방법
API_ONLY_RESELLER (601) 리셀러만 이용이 가능한 API입니다 리셀러 권한 계정 키로 호출합니다
RESELLER_SUSPENDED (343) 본사가 리셀러 사용을 멈췄습니다 (HTTP 403) 부트페이에 문의합니다
PROVIDER_NOT_FOUND (300) 본인 하위 가맹점이 아닙니다 (HTTP 404) provider_id 가 본인 하위 가맹점인지 확인합니다
PG_WAIT_NOT_FOUND (11000) 그 가맹점의 신청서가 아닙니다 (HTTP 404) onboarding_id 를 목록에서 다시 확인합니다

4등록 API 접수

리셀러가 자기 화면에서 판매자의 사업자 정보·서류·약관 동의를 받아, 리셀러 서버가 하위 가맹점의 PG 가맹 신청서를 직접 냅니다. 부트페이 신청 화면을 띄우지 않는 길입니다. 부트페이 화면으로 받고 싶다면 등록 폼 세션 발급 을 사용합니다.

동작 규칙

  • 순서는 약관 조회 → (서류가 있으면) 업로드 허가 받기 → storage 에 서류 올리기 → 신청서 접수​​입니다. 진행 상태는 등록 상태 조회 로 읽습니다.
  • 신청서와 약관 동의 기록은 한 번에 저장​​됩니다. 동의가 빠지거나 버전이 다르면 신청서도 남지 않습니다.
  • 약관 동의는 리셀러 화면에서 받으므로, 약관마다 동의 시각·동의한 사람의 IP·동의자​​를 함께 보냅니다. 이 값이 그대로 동의 기록이 됩니다.
  • 접수 응답은 등록 상태 조회의 한 항목과 같은 모양입니다(channel 은 api).
  • 지금 낼 수 있는 신청은 PG 가맹 신청(pg_join) 하나입니다. A형(대표가맹점) 리셀러는 아직 쓸 수 없습니다.
이 API 로 낸 접수는 웹훅·알림톡이 오지 않습니다

이 API 로 신청서를 내면 "접수" 리셀러 웹훅(seller.onboarding.changed)과 리셀러 담당자 알림톡이 나가지 않습니다. 접수 결과는 이 API 의 응답으로 바로 확인합니다. 그 뒤 부트페이가 심사 상태를 바꾸면(서류 요청·반려·승인 등) 그때부터는 다른 입구와 똑같이 웹훅·알림톡이 갑니다.

서버에서만 부르고, 받은 개인정보를 남기지 않습니다

리셀러 계정 키(Client Key·Secret Key)로 부르므로 리셀러 서버에서만 호출합니다. 사업자등록증·정산 계좌·담당자 연락처를 리셀러가 직접 받는 길이라, 영업대행계약의 개인정보 처리 위탁 조항을 지키고 요청 본문을 로그·에러 리포팅에 남기지 않습니다.

4-1. 전체 흐름

단계 호출 받는 것
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 신청서 상태

4-2. 약관 조회

판매자에게 보여 줄 약관의 현재 버전과 원문을 읽습니다. 접수 때 agreements[] 에는 여기서 받은 code·version 을 그대로 넣습니다. 약관이 바뀌면 이전 버전으로 낸 접수는 거부됩니다.

GEThttps://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 빠지면 접수가 거부되는 필수 약관인지

4-3. 서류 업로드 허가

사업자등록증을 올릴 수 있는 10분짜리 허가를 받습니다. 파일은 부트페이 API 서버를 거치지 않고 리셀러 서버가 upload_url 로 직접 올립니다.

POSThttps://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)로 거부합니다 — 커머스 연동 재시도 뒤 다시 올립니다.

4-4. 신청서 접수

POSThttps://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)도 필수입니다. 글자 칸에는 문자열이나 숫자만 보냅니다.

다시 보내도 한 건만 — Idempotency-Key

응답을 못 받아 같은 접수를 다시 보낼 때 신청서가 두 건 생기지 않게 하려면, 접수마다 새 값(예: 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, ... }javascript

응답

접수·보완 응답은 등록 상태 조회 목록의 한 항목과 같습니다.

{
  "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-5. 보완·재제출

서류 보완 요청을 받았거나 내용을 고칠 때 같은 신청서를 다시 냅니다. 판매자가 부트페이 콘솔이나 등록 폼으로 낸 그 가맹점의 신청서도 고칠 수 있습니다.

PUThttps://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. 신청서 접수로 다시 냅니다. 응답은 접수와 같습니다.

4-6. 에러 코드

코드 메시지 대처 방법
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 업로드 — 하위 가맹점의 커머스 연동이 없습니다 커머스 연동 재시도 뒤 다시 올립니다

5신청서 서류 열기

하위 가맹점의 PG 가맹 신청서에 올라간 서류 목록과, 서류마다 3분 동안만 열 수 있는 열람 주소를 돌려줍니다. 신청서 id 는 등록 상태 조회 의 onboarding_id입니다.

동작 규칙

  • 지금 신청서에 붙는 서류는 사업자등록증 하나입니다. 올리지 않았으면 list 가 빈 배열입니다.
  • url 은 3분 뒤 막힙니다. 저장해 두지 말고 열 때마다 이 API 를 다시 부릅니다.
  • 서류는 개인정보입니다. url 을 로그·에러 리포팅·브라우저 주소 공유에 남기지 않습니다.
  • 부를 때마다 열람 기록이 신청서에 남습니다(리셀러가 열었다는 기록).
  • 판매자가 콘솔에서 직접 낸 신청서도 본인 하위 가맹점 것이면 열 수 있습니다.
  • 다른 가맹점이나 다른 리셀러 하위의 신청서는 없는 것과 같은 PG_WAIT_NOT_FOUND입니다.
  • 열기 전에 서류가 그 하위 가맹점이 올린 파일인지 확인합니다. 아니거나 지워진 파일이면 그 서류는 list 에 나오지 않습니다.

API 정보

GEThttps://api.bootpay.co.kr/v2/reseller/seller/:provider_id/onboarding/:onboarding_id/documentsBasic Auth (리셀러 계정 키)

5-1. 요청 파라미터

파라미터 타입 필수 설명
provider_id String 필수 하위 가맹점 provider_id (URL 경로)
onboarding_id String 필수 신청서 id (URL 경로) — 등록 상태 조회 의 onboarding_id

5-2. 코드 예제

import fetch from 'node-fetch'

const RESELLER_AUTH = 'Basic ' + Buffer
  .from(`${process.env.BOOTPAY_RESELLER_CLIENT_KEY}:${process.env.BOOTPAY_RESELLER_SECRET_KEY}`)
  .toString('base64')

const providerId   = '65a1c0aa8f1b5b00367a0001'
const onboardingId = '65a1c0aa8f1b5b00367a0050'

const res = await fetch(`https://api.bootpay.co.kr/v2/reseller/seller/${providerId}/onboarding/${onboardingId}/documents`, {
    headers: { 'Authorization': RESELLER_AUTH }
})
const { list } = await res.json()
// list[0].url 을 바로 열어 보여 준다 — 저장하지 않는다javascript

5-3. 응답

{
  "onboarding_id": "65a1c0aa8f1b5b00367a0050",
  "list": [
    {
      "kind":        "business_license",
      "file_name":   "사업자등록증.pdf",
      "mime_type":   "application/pdf",
      "size":        482113,
      "uploaded_at": "2026-10-20T11:02:00+09:00",
      "url":         "https://s.bootapi.com/api/v1/secure_documents/example_file_id/download?expires=1793090063&p=65a1c0aa8f1b5b00367a0001&sig=example_signature",
      "expires_at":  "2026-10-27T10:03:00+09:00",
      "expires_in":  180
    }
  ]
}json
필드 타입 필수 설명
onboarding_id String 필수 신청서 id
list Array 필수 서류 목록. 서류가 없으면 []
kind String 필수 서류 종류. 지금은 business_license(사업자등록증)
file_name String 선택 올린 파일 이름
mime_type String 선택 파일 형식
size Number 선택 파일 크기(바이트)
uploaded_at String 선택 올린 시각
url String 필수 열람 주소. 3분 뒤 막힙니다
expires_at String 필수 url 이 막히는 시각
expires_in Number 필수 url 유효 시간(초). 180

5-4. 에러 코드

코드 메시지 대처 방법
API_ONLY_RESELLER (601) 리셀러만 이용이 가능한 API입니다 리셀러 권한 계정 키로 호출합니다
API_SCOPE_INVALID 허용되지 않은 API 권한의 키입니다 (HTTP 401) 키에 이 API 의 리셀러 스코프 를 주거나 전체 권한(supervisor) 키로 호출합니다
PROVIDER_NOT_FOUND (300) 본인 하위 가맹점이 아닙니다 (HTTP 404) provider_id 가 본인 리셀러 하위인지 확인합니다
PG_WAIT_NOT_FOUND (11000) 그 가맹점의 신청서가 아닙니다 (HTTP 404) 등록 상태 조회 에서 onboarding_id 를 다시 확인합니다
COMMERCE_REQUEST_FAILED (8182) 서류 주인을 확인하지 못했습니다(연동 서버 오류). 열람 주소를 주지 않습니다 잠시 뒤 다시 부릅니다

함께 보기