본문 바로가기
빌링플러스개발자센터
홈 로그인 도입 상담

연동 가이드

등록창으로 카드 등록

카드번호가 가맹점 서버를 지나지 않게 회원이 직접 카드를 등록하는 등록창 연동 방법.

등록창은 가맹점 사이트 위에 띄우는 빌링플러스 팝업입니다. 회원이 그 창에 직접 카드번호를 넣고 정기결제(또는 수시결제)에 동의합니다. 카드번호는 회원 브라우저에서 등록창으로만 들어가고 가맹점 서버를 지나지 않습니다. 가맹점은 카드 정보 없이 "등록됐다" 는 결과만 받습니다.

흐름

  1. 가맹점 서버가 POST /api/v1/register-windows 로 창을 만듭니다(계약 조건을 함께 보냅니다). 응답으로 window_url 을 받습니다.
  2. 가맹점 웹페이지가 window_url 을 팝업으로 엽니다.
  3. 회원이 창에서 정보를 확인하고 카드를 등록한 뒤 동의합니다.
  4. 완료되면 창이 가맹점 웹페이지로 완료 메시지를 보내고 닫힙니다.
  5. 가맹점 서버가 GET /api/v1/register-windows/{id} 또는 웹훅으로 결과를 확정합니다.

준비 — 홈페이지 주소 등록

빌링플러스 화면의 설정 > 가맹점 정보에 가맹점 홈페이지 주소를 등록합니다. 창을 만들 때 보내는 return_url 의 도메인이 여기 등록된 도메인과 같아야 합니다. 등록된 주소가 없거나 도메인이 다르면 422 이며, 메시지에 허용된 도메인 목록이 나옵니다.

return_url 의 출처(https://도메인)는 완료 메시지를 받을 창을 정하는 데 쓰입니다. 가맹점 사이트가 아닌 곳으로 결과가 새지 않게 하는 장치입니다.

창 만들기 — POST /api/v1/register-windows

필드필수설명
return_url필수가맹점 사이트 주소(등록한 도메인). 완료 메시지를 받을 출처가 됩니다.
item_name필수품목명(자유 입력). 회원이 창에서 봅니다.
billing_modeRECURRING(정기결제, 기본) 또는 ON_DEMAND(수시결제 동의)
amount정기결제면 필수회차마다 걷을 금액(원, 1 이상 정수)
billing_day정기결제면 필수청구일 1~31. 그 달에 없는 날(31일 등)은 말일에 청구합니다.
cycleMONTHLY(기본) · QUARTERLY · HALF_YEARLY · YEARLY
term_months약정 개월. 비우면 종료일 없는 계약
limit_once · limit_month수시결제면 필수한 번에·한 달에 걷을 수 있는 최대 금액(원). 월 한도는 1회 한도보다 작을 수 없습니다.
customer_id이미 있는 회원의 계약으로 만들 때. 비우면 회원이 창에서 정보를 입력해 새 회원이 됩니다.
customer_name · customer_phone · customer_email새 회원일 때 창에 미리 채워 둘 값
merchant_ref가맹점 쪽 주문·회원 식별값(100자). 결과와 목록 필터에 그대로 돌아옵니다.

계약 조건은 창을 열기 전에 검사합니다. 조건이 틀리면 창이 만들어지지 않고 422 가 옵니다 — 회원이 카드까지 넣은 뒤에 실패하는 일이 없게 하기 위해서입니다.

요청 본문
{
  "return_url": "https://shop.example.com/membership",
  "item_name": "월 회비",
  "amount": 29900,
  "billing_day": 10,
  "cycle": "MONTHLY",
  "term_months": 12,
  "merchant_ref": "member-1024",
  "customer_name": "홍길동",
  "customer_phone": "01012345678"
}
응답 201
{
  "id": 341,
  "status": "OPEN",
  "merchant_ref": "member-1024",
  "item_name": "월 회비",
  "amount": 29900,
  "cycle": "MONTHLY",
  "billing_day": 10,
  "term_months": 12,
  "return_url": "https://shop.example.com/membership",
  "customer_id": null,
  "subscription_id": null,
  "contract_no": null,
  "billing_mode": "RECURRING",
  "limit_once": null,
  "limit_month": null,
  "charge_consent_id": null,
  "charge_consent_no": null,
  "expires_at": "2026-10-28T14:30:00+09:00",
  "opened_at": null,
  "used_at": null,
  "created_at": "2026-10-28T14:00:00+09:00",
  "window_url": "https://…/window/Q2xv…bmU/",
  "window_url_reissued": false
}

window_url 은 이 응답에만 한 번 나옵니다. 조회 API 로는 다시 받을 수 없습니다. 창은 30분 동안 열 수 있습니다. 응답을 받지 못했으면 같은 멱등키로 다시 보내세요 — 아직 열리지 않은 창이면 주소를 새로 받습니다(아래 '알아 둘 것').

window_url 을 문자·메일로 회원에게 보내지 마세요. 메시지 속 링크에서 카드번호를 받는 모양은 스미싱과 같아 보입니다. 회원이 가맹점 사이트에 들어와 있을 때 팝업으로만 엽니다.

브라우저는 사용자 클릭 직후에 연 창만 허용합니다. 서버 응답을 기다린 뒤 window.open 을 부르면 팝업 차단에 걸리기 쉬우니, 클릭하는 순간 빈 창을 먼저 열고 주소를 나중에 넣으세요. API 호출은 시크릿 키가 있는 가맹점 서버에서 합니다.

가맹점 웹페이지 (JavaScript)
// 가맹점 웹페이지(브라우저) — 버튼을 누른 그 자리에서 빈 팝업을 먼저 엽니다(팝업 차단 방지).
document.getElementById('register-card').addEventListener('click', async () => {
  const popup = window.open('about:blank', 'billingplus_register', 'width=480,height=760');

  // 가맹점 서버가 POST /register-windows 를 부르고 window_url 과 id 만 돌려주는 내 API
  const res = await fetch('/my-server/register-window', { method: 'POST' });
  if (!res.ok) { popup.close(); showError('잠시 후 다시 시도해 주세요.'); return; }   // showError: 내 화면의 오류 표시
  const { windowId, windowUrl } = await res.json();

  const expectedOrigin = new URL(windowUrl).origin;
  window.addEventListener('message', function onMessage(event) {
    if (event.origin !== expectedOrigin) return;                  // 다른 출처의 메시지는 무시
    if (!event.data || event.data.type !== 'billingplus:register') return;
    window.removeEventListener('message', onMessage);
    // 화면 전환용 신호입니다. 확정은 서버에서 다시 확인합니다(아래).
    location.href = `/my-page/card-registered?window=${windowId}`;
  });

  popup.location.href = windowUrl;
});

등록이 끝나면 창이 window.opener.postMessage 로 아래 메시지를 보냅니다. 받는 출처는 return_url 의 출처로 정해져 있습니다.

완료 메시지 — 정기결제
{
  "type": "billingplus:register",
  "ok": true,
  "data": {
    "billingMode": "RECURRING",
    "contractNo": "C-2026-0001",
    "customerCode": "M-0001",
    "merchantRef": "member-1024",
    "itemName": "월 회비",
    "amount": 29900,
    "billingDay": 10,
    "nextBillingDate": "2026-11-10"
  }
}
완료 메시지 — 수시결제 동의
{
  "type": "billingplus:register",
  "ok": true,
  "data": {
    "billingMode": "ON_DEMAND",
    "chargeConsentId": 77,
    "chargeConsentNo": "Q26100001",
    "customerCode": "M-0001",
    "merchantRef": "member-1024",
    "itemName": "이용 요금",
    "limitOnce": 50000,
    "limitMonth": 200000
  }
}

완료 메시지는 화면을 넘기는 신호로만 쓰세요. 회원이 창을 그냥 닫거나 브라우저가 메시지를 막으면 오지 않을 수 있고, 브라우저에서 온 값은 바뀔 수 있습니다. 이용권 지급 같은 처리는 서버에서 GET /api/v1/register-windows/{id} 의 status 가 USED 인지 확인하거나, 웹훅(subscription.created · charge_consent.created · billing_key.registered)을 받은 뒤에 합니다.

상태 확인

status뜻
OPEN아직 쓸 수 있는 창(30분 안, 등록 전)
USED등록 완료. 정기결제면 subscription_id · contract_no, 수시결제면 charge_consent_id · charge_consent_no 가 채워집니다.
EXPIRED시간이 지났거나 닫은 창. 다시 열 수 없습니다 — 새로 만드세요.

opened_at 으로 회원이 창을 열었는지 알 수 있습니다. 목록은 GET /api/v1/register-windows?status=OPEN&merchant_ref=member-1024 처럼 거릅니다.

창 닫기 — POST /api/v1/register-windows/{id}/close

조건을 잘못 넣었거나 회원이 그만뒀을 때 창을 무효로 만듭니다. 본문은 비워도 되지만 POST 이므로 Idempotency-Key 는 필요합니다.

이미 등록이 끝난 창은 닫을 수 없습니다(422). 창을 닫아도 만들어진 계약은 그대로라, 그대로 두면 다음 청구일에 결제됩니다. 계약을 멈추려면 POST /api/v1/subscriptions/{id}/cancel 로 해지하고, 수시결제 동의는 POST /api/v1/charge-consents/{id}/revoke 로 해지하세요.

수시결제 동의 창

billing_mode 를 ON_DEMAND 로 보내면 정기결제 계약 대신 수시결제 동의를 받습니다. 회원은 창에서 "한 번에 최대 얼마, 한 달에 최대 얼마" 를 보고 동의하며, 이때 amount · billing_day 는 보내지 않아도 됩니다. 등록이 끝나면 charge_consent_id 로 필요할 때 결제를 요청합니다 — 수시결제 동의로 결제

요청 본문 — 수시결제 동의
{
  "return_url": "https://shop.example.com/membership",
  "item_name": "이용 요금",
  "billing_mode": "ON_DEMAND",
  "limit_once": 50000,
  "limit_month": 200000,
  "customer_id": 1024
}

알아 둘 것

  • 등록창 경로는 결제 경로와 같은 낮은 호출 한도를 씁니다 — 레이트리밋
  • 같은 멱등키로 다시 보내도 창은 하나만 만들어집니다. 빌링플러스는 window_url 을 저장하지 않으므로 재요청 응답은 창의 지금 상태입니다.
    • 아직 아무도 열지 않은 창 — 새 주소를 발급합니다(window_url_reissued: true). 앞서 받은 주소는 그 순간부터 열리지 않으니 새 주소로 팝업을 엽니다. 창의 만료 시각은 늘어나지 않습니다.
    • 이미 열린 창 · 만료 · 사용된 창 — window_url 이 null 입니다. 상태(status · opened_at)를 보고, 새 창이 필요하면 열린 창을 닫고(POST …/{id}/close) 새 멱등키로 다시 만드세요.
  • 지정 월 청구(예: 3·6·9·12월)는 등록창으로 만들 수 없습니다. 빌링플러스 화면에서 계약을 만드세요.