연동 가이드
등록창으로 카드 등록
카드번호가 가맹점 서버를 지나지 않게 회원이 직접 카드를 등록하는 등록창 연동 방법.
등록창은 가맹점 사이트 위에 띄우는 빌링플러스 팝업입니다. 회원이 그 창에 직접 카드번호를 넣고 정기결제(또는 수시결제)에 동의합니다. 카드번호는 회원 브라우저에서 등록창으로만 들어가고 가맹점 서버를 지나지 않습니다. 가맹점은 카드 정보 없이 "등록됐다" 는 결과만 받습니다.
흐름
- 가맹점 서버가
POST /api/v1/register-windows로 창을 만듭니다(계약 조건을 함께 보냅니다). 응답으로window_url을 받습니다. - 가맹점 웹페이지가
window_url을 팝업으로 엽니다. - 회원이 창에서 정보를 확인하고 카드를 등록한 뒤 동의합니다.
- 완료되면 창이 가맹점 웹페이지로 완료 메시지를 보내고 닫힙니다.
- 가맹점 서버가
GET /api/v1/register-windows/{id}또는 웹훅으로 결과를 확정합니다.
준비 — 홈페이지 주소 등록
빌링플러스 화면의 설정 > 가맹점 정보에 가맹점 홈페이지 주소를 등록합니다. 창을 만들 때 보내는 return_url 의 도메인이 여기 등록된 도메인과 같아야 합니다. 등록된 주소가 없거나 도메인이 다르면 422 이며, 메시지에 허용된 도메인 목록이 나옵니다.
return_url 의 출처(https://도메인)는 완료 메시지를 받을 창을 정하는 데 쓰입니다. 가맹점 사이트가 아닌 곳으로 결과가 새지 않게 하는 장치입니다.
창 만들기 — POST /api/v1/register-windows
| 필드 | 필수 | 설명 |
|---|---|---|
return_url | 필수 | 가맹점 사이트 주소(등록한 도메인). 완료 메시지를 받을 출처가 됩니다. |
item_name | 필수 | 품목명(자유 입력). 회원이 창에서 봅니다. |
billing_mode | RECURRING(정기결제, 기본) 또는 ON_DEMAND(수시결제 동의) | |
amount | 정기결제면 필수 | 회차마다 걷을 금액(원, 1 이상 정수) |
billing_day | 정기결제면 필수 | 청구일 1~31. 그 달에 없는 날(31일 등)은 말일에 청구합니다. |
cycle | MONTHLY(기본) · 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"
}{
"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 호출은 시크릿 키가 있는 가맹점 서버에서 합니다.
// 가맹점 웹페이지(브라우저) — 버튼을 누른 그 자리에서 빈 팝업을 먼저 엽니다(팝업 차단 방지).
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월)는 등록창으로 만들 수 없습니다. 빌링플러스 화면에서 계약을 만드세요.
개발자센터