PortOne · 구독 결제

PortOne 구독결제 API

상품·할인·고객·구독·인보이스를 다루는 정기결제 API입니다. 인보이스는 청구 예정 일시에 생성과 동시에 결제되며, 결제수단은 카드 빌링키를 사용합니다.

사전 공개용 초안입니다. 정식 릴리스 전 자료로, 필드명과 동작이 바뀔 수 있습니다. 연동 착수 전에 최종 릴리스 문서를 확인해 주세요.

공통 규약

인증
Authorization: PortOne {API_SECRET}
요청 형식
변경 요청(POST·PUT·PATCH·DELETE)은 JSON 본문으로 보냅니다. 조회 요청(GET)은 같은 형태의 JSON을 requestBody 쿼리 파라미터에 담습니다 — 예: GET /subscriptions?requestBody={"page":{"number":0,"size":20}}
멱등성
Idempotency-Key 헤더를 변경 요청에 사용합니다. 같은 키로 다시 호출하면 저장된 응답을 그대로 돌려주며 유효 기간은 24시간입니다. 처리 중인 키로 다시 호출하면 거절됩니다. 결제가 일어나는 요청(고객 등록·구독 생성·인보이스 생성·재시도·재생성)에는 반드시 사용하시기를 권장합니다.
리소스 id
상품·할인·고객·구독의 id는 가맹점이 직접 정합니다. 영문·숫자와 . _ -만 쓸 수 있고 64자까지입니다. 고객을 뺀 나머지는 생략하면 자동으로 부여합니다.
페이징
요청 page { number, size } — number는 0부터. 응답 pageInfo { number, size, totalCount }.
에러
{ type, message, … } 형태이며 type은 대문자 스네이크 표기입니다. 결제 실패는 PAYMENT_FAILED { paymentId, pgCode, pgMessage }로 내려갑니다.
일시
ISO 8601에 KST 오프셋을 포함합니다. 분 단위로 의미를 갖는 필드(billingAt 등)는 초가 0입니다.
금액
통화의 최소 단위 정수로 표기하고 currency(ISO 4217)를 함께 내려줍니다. 부가세는 KRW에서만 계산합니다 — 청구 금액 ÷ 1.1이 공급가액, 나머지가 부가세(원 단위 반올림)이며, KRW 외 통화는 부가세가 0원입니다. 할인으로 청구 금액이 0원이 되면 결제를 시도하지 않고 완료 처리합니다.
결제수단
카드 빌링키만 지원합니다. 인보이스의 결제수단은 CARD_BILLING으로 고정입니다.
청구 처리
청구 예정 일시가 지나면 몇 분 안에 인보이스를 만들고 결제합니다. 정확히 그 시각은 아닙니다. 지난 회차를 건너뛰지 않으므로, 어떤 이유로 청구가 밀렸더라도 회차마다 인보이스가 하나씩 만들어집니다.
결과 확인
결제 요청은 보냈지만 결과를 받지 못한 인보이스는 PENDING으로 잠시 머뭅니다. 10분쯤 뒤 결제 상태를 확인해 완료나 실패로 정리하므로 같은 건이 두 번 결제되지 않습니다.
변경 반영
구독·상품·할인 변경은 모두 다음 결제부터 반영됩니다. 일할 계산(비례배분)은 하지 않으며 이미 결제된 인보이스는 바뀌지 않습니다.
metadata
상품·고객·구독에 가맹점이 원하는 키-값을 붙일 수 있습니다. 목록 조회에서 필터로도 쓰이며, 보낸 키-값이 모두 일치하는 건만 걸러집니다. 키는 50개까지, 키는 40자까지, 값은 500자까지입니다.

시작 흐름

고객을 등록하려면 빌링키가 먼저 있어야 합니다. 빌링키는 브라우저 SDK에서 발급받습니다.

  1. 빌링키 발급 — 브라우저 SDK의 빌링키 발급 메서드(requestIssueBillingKey)로 카드 빌링키를 발급받습니다.
  2. 고객 등록 — 발급받은 빌링키로 POST /customers를 호출해 고객을 등록합니다. subscription을 함께 보내면 구독까지 한 번에 만들 수 있습니다.
  3. 구독 생성 — 이미 등록된 고객에 구독을 추가할 때는 POST /subscriptions를 사용합니다.

빌링키를 교체할 때는 SDK로 새 빌링키를 발급받은 뒤 POST /customers/{id}/payment-method/revoke로 기존 빌링키를 해지하고, POST /customers로 새 빌링키를 등록합니다. 기존 빌링키가 살아 있는 상태에서 새 빌링키를 등록하면 CustomerHasBillingKey로 거절됩니다.

상품 /products

구독을 등록하기 위해 상품을 등록합니다. 결제 주기와 가격은 상품에서 정해집니다.

product 필드

필드타입설명변경
idstring가맹점이 지정하거나 자동 부여불가
namestring상품명PATCH
descriptionstring?설명PATCH
pricingTypeenumRECURRING · ONE_TIME불가
intervalenum?결제 주기. RECURRING일 때만 존재불가
amountint1 이상. currency와 함께 사용PATCH (다음 결제부터)
currencystringISO 4217불가
billingPausedbool청구 중지 여부pause · resume-billing
archivedbool아카이브 여부archive (복구 불가)
metadataobject가맹점이 붙이는 키-값PATCH

엔드포인트

POST/products상품 생성
요청
{
  "id": "prd_english",
  "name": "영어 심화",
  "description": "주 2회 화상 수업",
  "pricingType": "RECURRING",
  "interval": "MONTHLY",
  "amount": 99000,
  "currency": "KRW",
  "metadata": { "category": "class" }
}
규칙
  • id·description·metadata는 선택입니다. id를 생략하면 자동으로 부여합니다.
  • RECURRING이면 interval이 필수이고, ONE_TIME이면 넣을 수 없습니다.
  • amount는 1 이상입니다.
  • 이미 있는 id면 409입니다.
응답
product
GET/products/{id}상품 조회
규칙

아카이브된 상품도 조회됩니다. 없으면 404입니다.

응답
product
GET/products상품 목록
요청
{ "page": { "number": 0, "size": 20 }, "archived": false, "metadata": { "category": "class" } }
규칙

archived 기본값은 false로 아카이브된 상품은 제외됩니다. 청구가 중지된 상품은 billingPaused: true로 목록에 포함됩니다. metadata는 선택이며 보낸 키-값이 모두 일치하는 상품만 걸러냅니다. 생성일 역순입니다.

응답
product[] + pageInfo
PATCH/products/{id}상품 수정
요청
{ "name": "영어 심화 (2026)", "description": "…", "amount": 109000 }
규칙
  • 보낸 필드만 바뀝니다.
  • id·pricingType·interval은 바꿀 수 없습니다.
  • 금액 변경은 다음 결제부터 적용됩니다. 인보이스는 결제 시점의 가격을 씁니다.
  • 아카이브된 상품은 409입니다.
응답
product
POST/products/{id}/pause-billing청구 중지
규칙

이 상품이 담긴 활성 구독의 인보이스 생성과 결제가 멈추고, 신규 구독 등록도 막힙니다. 구독과 고객 정보는 바뀌지 않습니다. 이미 중지 상태면 아무 일도 하지 않습니다.

응답
product
POST/products/{id}/resume-billing청구 재개
규칙

이 상품이 담긴 구독은 다음에 도래하는 청구일부터 생성·결제를 재개합니다. 재개하는 시점에 각 구독의 다음 청구 예정일을 지금 이후 첫 회차로 다시 잡으므로, 중지 기간에 지나간 회차는 청구하지 않습니다. 다시 잡은 날짜가 구독 종료일을 넘으면 그 구독은 종료됩니다. 중지 상태가 아니면 아무 일도 하지 않습니다.

응답
product
POST/products/{id}/archive아카이브
규칙

목록에서 숨기고 신규 구독 등록을 막습니다. 이미 이 상품을 담고 있는 구독은 계속 청구됩니다. 되돌릴 수 없고 같은 id를 다시 쓸 수 없습니다.

응답
product

할인 /discounts

구독에 붙여 인보이스 금액을 깎는 규칙입니다. 지속 기간은 날짜가 아니라 회차로 관리합니다. 한 구독에 서로 다른 할인을 여러 개 붙일 수 있습니다.

discount 필드

필드타입설명변경
idstring가맹점이 지정하거나 자동 부여불가
namestring인보이스와 화면에 표시되는 이름PATCH
typeenumPERCENT · AMOUNT불가
valueintPERCENT는 1~100, AMOUNT는 정수 금액불가
currencystring?ISO 4217. AMOUNT일 때 필수이며 PERCENT에는 없습니다불가
appliesToobject{ "type": "SUBSCRIPTION" }이면 인보이스 전체에, { "type": "PRODUCTS", "productIds": [...] }이면 지정한 상품 항목에만 적용. 자세한 계산은 아래 할인 계산을 봐 주세요불가
durationobject{ "type": "ONCE" } · { "type": "CYCLES", "count": n } · { "type": "FOREVER" }불가
redeemBydate?적용 마감일. 이 날이 지나면 구독에 새로 붙일 수 없습니다PATCH
archivedbool보관 여부archive · unarchive

할인 계산

  • 정률(PERCENT)을 먼저 적용합니다. 구독에 붙인 순서대로, 적용 범위에 드는 항목마다 금액을 곱해 깎습니다. 항목별로 1원 단위 반올림합니다.
  • 정액(AMOUNT)은 그다음입니다. 전체 범위 할인은 합계에서 한 번 빼고, 상품 지정 할인은 대상 항목마다 한 번씩 뺍니다(수량과 무관). 각 항목 금액과 남은 합계를 넘지 않습니다.
  • 적용 범위에 드는 항목이 인보이스에 하나도 없으면 그 할인은 반영되지 않고 잔여 회차도 줄지 않습니다.
  • 합계가 0원 아래로 내려가지 않습니다. 0원이 되면 결제 없이 완료 처리합니다.
예를 들어 영어 99,000원과 교재 15,000원이 담긴 인보이스에 ① 전체 10%와 ② 교재 한정 3,000원이 붙어 있다면, ①로 영어 89,100원·교재 13,500원이 되어 합계 102,600원, 이어서 ②로 교재가 10,500원이 되어 합계 99,600원입니다.

appliedDiscount 필드 구독에 붙은 할인

필드타입설명
discountIdstring붙어 있는 할인의 참조
appliedAtdatetime구독에 연결한 일시
remainingCountint | null남은 적용 회차. ONCE는 1, CYCLES(n)은 n에서 시작해 할인이 실제로 반영된 인보이스가 만들어질 때마다 1씩 줄어듭니다. FOREVER는 null이며 소진되지 않습니다
statusenumACTIVE · EXHAUSTED(회차 소진) · REMOVED(제거됨)

엔드포인트

POST/discounts할인 생성
요청
{
  "id": "disc_welcome10",
  "name": "첫 가입 10%",
  "type": "PERCENT",
  "value": 10,
  "appliesTo": { "type": "SUBSCRIPTION" },
  "duration": { "type": "CYCLES", "count": 3 },
  "redeemBy": "2026-12-31"
}
규칙
  • id·redeemBy·appliesTo는 선택입니다. appliesTo 기본값은 SUBSCRIPTION입니다.
  • PRODUCTS로 지정하면 productIds가 하나 이상 필요하고, 그 상품이 있고 아카이브되지 않았어야 합니다.
  • duration.type은 ONCE·CYCLES·FOREVER 중 하나이고, CYCLES는 count(1 이상)가 필수입니다.
  • PERCENT는 1~100이며 currency를 넣지 않습니다. AMOUNT는 1 이상이고 currency가 필수입니다.
  • redeemBy가 오늘(KST)보다 이전이면 거절합니다.
응답
discount
GET/discounts/{id}할인 조회
규칙

보관된 할인도 조회됩니다. 없으면 404입니다.

응답
discount
GET/discounts할인 목록
요청
{ "page": { "number": 0, "size": 20 }, "archived": false }
규칙

archived 기본값은 false입니다. 생성일 역순입니다.

응답
discount[] + pageInfo
PATCH/discounts/{id}할인 수정
요청
{ "name": "첫 가입 10% (연장)", "redeemBy": "2027-03-31" }
규칙

이름과 적용 마감일만 바꿀 수 있습니다. 할인 조건(type·value·currency·duration·appliesTo)은 만든 뒤에 바꿀 수 없습니다 — 조건을 바꾸려면 새 할인을 만들어 주세요.

응답
discount
POST/discounts/{id}/archive할인 보관
규칙

새로 연결하는 것만 막습니다. 이미 붙어 있는 구독에서는 잔여 회차까지 그대로 적용됩니다. 이미 보관 상태면 아무 일도 하지 않습니다.

응답
discount
POST/discounts/{id}/unarchive보관 해제
규칙

다시 신규 연결이 가능해집니다.

응답
discount

고객 /customers

고객은 빌링키와 함께 등록합니다. 빌링키 없이 고객만 만들 수는 없으며, 고객당 빌링키는 하나입니다.

customer 필드

필드타입설명변경
idstring가맹점이 지정. 항상 필수불가
namestring신규 등록 시 필수POST · PATCH
emailstring?선택. 결제 결과 인보이스를 받는 주소이며, 메일을 보내는 구독을 만들려면 반드시 있어야 합니다(없으면 CustomerEmailRequired)POST · PATCH
phonestring?선택POST · PATCH
businessRegistrationNumberstring?사업자번호. 선택, 숫자 10자리POST · PATCH
statusenumACTIVE 하나뿐입니다시스템
billingKeystring?현재 빌링키. 고객당 1개등록 · 해지 시 null
metadataobject가맹점이 붙이는 키-값POST · PATCH

엔드포인트

POST/customers고객 등록 · 결제수단 등록
요청
{
  "id": "cust_001",
  "name": "홍길동",
  "email": "hong@example.com",
  "phone": "010-1234-5678",
  "businessRegistrationNumber": "1234567890",
  "billingKey": "billing-key-…",
  "metadata": { "grade": "vip" },

  "subscription": {
    "items": [ { "productId": "prd_english", "quantity": 1 } ],
    "discountIds": ["disc_welcome10"]
  }
}
규칙
  • 고객을 새로 만들거나, 이미 있으면 보낸 필드만 갱신합니다. billingKey는 항상 필수이고 신규 고객이면 name도 필수입니다.
  • 빌링키는 그 상점이 소유한 카드 빌링키여야 합니다. 아니면 BillingKeyNotFound입니다.
  • 다른 고객이 쓰고 있는 빌링키면 BillingKeyAlreadyRegistered { customerId }입니다.
  • 이 API는 등록만 합니다. 살아 있는 빌링키를 가진 고객이면 CustomerHasBillingKey로 거절하므로 교체하려면 먼저 해지해야 합니다. 이미 해지된 빌링키를 들고 있으면 새 빌링키로 바뀝니다.
  • subscription을 함께 보내면 구독까지 한 번에 만듭니다. 필드는 구독 생성과 같고, 구독이 실패하면 고객도 만들어지지 않습니다.
응답
customer (+ subscription)
GET/customers/{id}고객 조회
규칙

없으면 404입니다.

응답
customer
GET/customers고객 목록
요청
{ "page": { "number": 0, "size": 20 }, "metadata": { "grade": "vip" } }
규칙

metadata는 선택입니다. 생성일 역순입니다.

응답
customer[] + pageInfo
GET/customers/{id}/subscriptions고객의 구독 목록
요청
{ "page": { "number": 0, "size": 20 }, "status": "ACTIVE" }
규칙

status는 선택입니다. 생성일 역순이고, 고객이 없으면 404입니다.

응답
subscription[] + pageInfo
GET/customers/{id}/invoices고객의 인보이스 목록
요청
{
  "page": { "number": 0, "size": 20 },
  "from": "2026-06-09T00:00:00+09:00",
  "to": "2026-09-07T23:59:59+09:00"
}
규칙

from·to는 선택이며 billingAt을 기준으로 거릅니다. 기본값은 from 90일 전, to 현재입니다. 그 고객의 모든 구독 인보이스를 billingAt 역순으로 돌려줍니다.

응답
invoice[] + pageInfo
PATCH/customers/{id}고객 정보 수정
요청
{
  "name": "홍길동",
  "email": "hong@example.com",
  "phone": "010-1234-5678",
  "businessRegistrationNumber": "1234567890",
  "metadata": { "grade": "vip" }
}
규칙

모든 필드가 선택이며 보낸 필드만 바뀝니다. id와 빌링키는 바꿀 수 없습니다. 이메일 변경은 다음 결제 결과 인보이스부터 반영됩니다. 사업자번호는 숫자 10자리 형식을 검사합니다.

응답
customer
POST/customers/{id}/payment-method/revoke결제수단 해지
요청
{ "reason": "고객 요청" }
규칙
  • reason은 선택이지만 일부 결제사가 필수로 요구하므로 채워 보내시길 권합니다.
  • 현재 빌링키를 해지하고 billingKey를 null로 만듭니다. 이미 해지된 빌링키는 성공으로 처리합니다.
  • 해지에 실패하면 BillingKeyHasSchedules 또는 BillingKeyRevokeFailed { pgCode, pgMessage }입니다.
  • 구독은 ACTIVE로 남지만, 청구일이 와도 빌링키가 없으면 인보이스를 만들지 않고 건너뜁니다.
  • 다시 등록하려면 SDK로 빌링키를 새로 발급받아 POST /customers를 호출합니다.
응답
customer

구독 /subscriptions

청구 시작일이 곧 구독 시작일입니다. 종료일을 지정하면 그 날짜까지만 청구하고 자동으로 종료합니다. 구독을 만들려면 고객에게 유효한 빌링키가 있어야 합니다.

subscription 필드

필드타입설명변경
idstring가맹점이 지정하거나 자동 부여불가
customerIdstring고객. 결제에는 그 고객의 현재 빌링키를 사용합니다불가
items[]{ productId, quantity }[]상품과 수량. 수량은 1 이상PATCH (다음 결제부터)
intervalenum?담긴 정기 상품들의 공통 주기. 일회성 상품만 있는 구독은 값이 없습니다불가
currencystring담긴 상품에서 정해집니다. 통화가 섞이면 구독을 만들 수 없습니다불가
billStartDatedate청구 시작일이자 구독 시작일. 청구 기준일의 최초 출처이며 기본값은 오늘불가
endDatedate?구독 종료일. 다음 청구 예정일이 이 날짜를 넘으면 인보이스를 만들지 않고 자동 종료합니다. 종료 판정은 청구 직후뿐 아니라 종료일 변경·재개·첫 회차 회수처럼 다음 청구일을 다시 잡는 모든 시점에 일어납니다. null이면 무기한PATCH
anchorDatedate청구 기준일. 기본값은 billStartDate이고 PATCH의 nextBillingDate로만 바뀝니다(말일 보정으로는 바뀌지 않습니다)PATCH (nextBillingDate)
anchorTimeHH:MM청구 시각. 구독을 만든 시각(KST 분 단위)으로 정해지며 가맹점이 입력하지 않습니다불가
lastBilledAtdatetime?마지막으로 청구한 일시시스템
nextBillingAtdatetime?다음 청구 예정 일시. 일시중지·종료 상태면 값이 없습니다시스템
discounts[]appliedDiscount[]붙어 있는 할인 목록. 개수 제한은 없고 같은 할인을 중복해서 붙일 수는 없습니다할인 추가 · 제거
sendInvoiceEmailbool결제 결과 인보이스 메일 발송 여부. 기본값은 고객 이메일이 있으면 truePATCH
statusenumACTIVE · INCOMPLETE · PAUSED · ENDEDpause · resume · cancel
endedAt
endReason
datetime? · enum?종료 일시와 사유 — CANCELED(해지) · ONE_TIME_EXHAUSTED(일회성 소진) · END_DATE_REACHED(종료일 도달)시스템
metadataobject가맹점이 붙이는 키-값PATCH

엔드포인트

POST/subscriptions구독 생성
요청
{
  "id": "sub_001",
  "customerId": "cust_001",
  "items": [ { "productId": "prd_english", "quantity": 1 } ],
  "discountIds": ["disc_welcome10"],
  "billStartDate": "2026-10-01",
  "endDate": "2027-09-30",
  "sendInvoiceEmail": true,
  "metadata": { "planName": "basic" }
}
규칙
  • id·discountIds·billStartDate·endDate·sendInvoiceEmail·metadata는 선택입니다.
  • billStartDate 기본값은 오늘이고 지난 날짜는 거절합니다. endDate는 billStartDate와 같거나 뒤여야 합니다.
  • 고객에게 유효한 빌링키가 있어야 합니다.
  • 담긴 정기 상품은 주기가 모두 같아야 하고, 통화도 모두 같아야 합니다. 아카이브·청구 중지 상품은 담을 수 없습니다.
  • 일회성 상품만으로도 만들 수 있으며, 이때는 결제 주기가 없습니다.
  • 할인은 있어야 하고, 보관되지 않았고, 적용 마감일 이내여야 합니다. PRODUCTS형은 그 상품이 items에 있어야 하고 같은 할인을 중복 지정할 수 없습니다.
  • sendInvoiceEmail 기본값은 고객 이메일이 있으면 true입니다. true인데 이메일이 없으면 CustomerEmailRequired입니다.
  • 같은 id로 다시 호출하면 409이고 결제가 다시 나가지 않습니다. id를 지정하지 않는다면 Idempotency-Key로 중복 생성을 막아 주세요.
당일 시작이면 첫 인보이스를 바로 결제합니다. 결제가 실패해도 요청은 성공(200)이고 구독은 INCOMPLETE, 인보이스는 FAILED로 남습니다. 그 인보이스를 재시도·재생성하거나 수동 완납으로 처리해 첫 회차 대금을 받으면 구독이 ACTIVE로 돌아가고, 다음 청구 예정일이 지금 이후 첫 회차로 다시 잡힙니다.
응답
subscription (+ 첫 invoice)
GET/subscriptions/{id}구독 조회
규칙

붙어 있는 할인 목록(각 항목의 잔여 회차와 상태 포함), nextBillingAt, endDate가 함께 내려갑니다. 없으면 404입니다.

응답
subscription
GET/subscriptions구독 목록
요청
{
  "page": { "number": 0, "size": 20 },
  "status": "ACTIVE",
  "customerId": "cust_001",
  "metadata": { "planName": "basic" }
}
규칙

status·customerId·metadata가 모두 선택입니다. 생성일 역순입니다.

응답
subscription[] + pageInfo
PATCH/subscriptions/{id}구독 수정
요청
{
  "items": [ { "productId": "prd_english", "quantity": 2 } ],
  "endDate": "2028-03-31",
  "nextBillingDate": "2026-10-20",
  "sendInvoiceEmail": false,
  "metadata": { "planName": "pro" }
}
규칙
  • 보낸 필드만 바뀌고, 변경은 다음 결제부터 반영됩니다. 결제 직전까지 바꿀 수 있습니다.
  • items는 전체 교체입니다. 추가·삭제·수량 변경을 한 번에 보냅니다.
  • 교체해도 주기와 통화는 기존 구독과 같아야 하고, 아카이브·청구 중지 상품은 담을 수 없습니다.
  • endDate를 null로 보내면 무기한으로 되돌립니다.
  • 종료일을 앞당겨 다음 청구 예정일이 그 날짜를 넘게 되면 남은 청구가 없으므로 구독이 그 자리에서 종료됩니다(END_DATE_REACHED).
  • 종료된 구독이면 409입니다.
nextBillingDate로 정기결제일을 옮깁니다. YYYY-MM-DD 날짜만 받고 시각은 넣지 않습니다. 오늘보다 뒤이고 종료일 이내여야 합니다. 지정한 날짜가 새 청구 기준일이 되며 기준 시각과 회차 번호는 그대로입니다. 이후 회차는 이 기준일에서 다시 계산됩니다.
응답
subscription
POST/subscriptions/{id}/discounts할인 추가
요청
{ "discountId": "disc_retention20" }
규칙

같은 할인이 이미 붙어 있으면 409이고, 다른 할인은 개수 제한 없이 계속 추가할 수 있습니다. PRODUCTS형이면 그 상품이 구독에 담겨 있어야 합니다. 적용 마감일과 보관 여부, 통화 일치를 검사합니다. 잔여 회차는 지속 방식에 따라 초기화됩니다. 다음 결제부터 적용되며 결제 직전까지 추가할 수 있습니다.

응답
subscription
GET/subscriptions/{id}/discounts붙은 할인 목록
규칙

구독에 붙어 있는 할인을 discountId·잔여 회차·상태·연결 일시와 함께 돌려줍니다. 제거된 할인은 빠집니다.

응답
appliedDiscount[]
DELETE/subscriptions/{id}/discounts/{discountId}할인 제거
규칙

지정한 할인 하나만 제거합니다. 다음 결제부터 그 할인이 적용되지 않습니다. 붙어 있지 않으면 아무 일도 하지 않습니다.

응답
subscription
POST/subscriptions/{id}/pause일시중지
규칙

본문이 없습니다. 기간을 정하지 않고 중지하며, 중지 중에는 인보이스 생성과 결제가 없습니다. ACTIVE가 아니면 409입니다.

응답
subscription
POST/subscriptions/{id}/resume재개
규칙

본문이 없습니다. 청구 기준일과 기준 시각을 그대로 유지하므로, 재개한 날짜와 무관하게 원래 정기결제일에 다음 인보이스를 만들어 결제합니다. 재개 시점에 즉시 결제하지 않으며 중지 중 건너뛴 회차도 소급하지 않습니다. 다시 잡은 청구일이 종료일을 넘으면 재개 대신 구독이 종료됩니다. PAUSED가 아니면 409입니다.

응답
subscription
POST/subscriptions/{id}/cancel해지
규칙

즉시 종료합니다. 이후 인보이스는 만들어지지 않습니다. 이미 종료된 구독이면 아무 일도 하지 않습니다.

응답
subscription
GET/subscriptions/{id}/preview-invoice다음 인보이스 미리보기
규칙
  • 다음 결제 때 만들어질 인보이스를 계산만 해서 보여주고 저장하지 않습니다.
  • 구독의 현재 상태(담긴 상품과 현재 가격, 일회성 상품 포함 여부, 할인, 수량)로 계산하므로 결제 직전까지 값이 바뀔 수 있습니다.
  • 다음 인보이스를 만들지 않을 상태면 null입니다 — 일시중지·종료, 청구 중지 상품 포함, 빌링키 없음, 종료일 초과.
응답
{ billingAt, sequence, items[], discounts[], totalAmount, supplyAmount, vatAmount }
GET/subscriptions/{id}/invoices구독의 인보이스 목록
요청
{
  "page": { "number": 0, "size": 20 },
  "from": "2026-06-09T00:00:00+09:00",
  "to": "2026-09-07T23:59:59+09:00"
}
규칙

from·to는 선택이며 billingAt 기준입니다. 기본값은 from 90일 전, to 현재이고 billingAt 역순으로 내려갑니다.

응답
invoice[] + pageInfo

인보이스 /invoices

인보이스를 미리 만들어 두지 않습니다. 청구 예정 일시가 되면 그 시점의 구독 상태로 인보이스를 만들고 곧바로 결제합니다. 수동 청구도 같은 방식으로 생성과 동시에 결제합니다.

invoice 필드

필드타입설명
idstring자동 부여
subscriptionIdstring수동 청구도 반드시 구독에 속합니다. 구독 없는 단독 인보이스는 없습니다
sequenceint?정기 청구의 회차 번호(1부터). 수동 청구는 null
regenerationSeqint기본 0. 재생성할 때마다 1씩 증가합니다
statusenumPENDING(결제 진행 중) · PAID · FAILED · CANCELED · PAID_MANUALLY
sourceenumAUTO(정기 청구) · MANUAL(수동 청구)
billingAtdatetime청구 기준 일시. 정기 청구는 구독의 기준일·기준 시각, 수동 청구는 요청한 시각
itemsarray생성 시점에 확정되는 스냅샷. productId+quantity+unitPrice 또는 name+unitPrice+quantity
discountsarray적용된 할인 스냅샷 { discountId, name, type, value, productIds?, discountAmt }. 정기 청구에만 자동 적용됩니다
currencystringISO 4217
supplyAmount
vatAmount
totalAmount
intKRW에서만 부가세를 계산하며, 그 외 통화는 vatAmount가 0입니다
memostring?수동 청구와 수동 완납 시 남기는 메모
paidAtdatetime?결제 완료 또는 수동 완납 처리 시각
paymentobject?결제 건 정보 { paymentId, billingKey, failure? }. 카드로 결제를 시도한 인보이스에만 있으며 0원 인보이스와 수동 완납 건에는 없습니다. paymentId로 결제 내역을 조회할 수 있습니다
lastFailureobject?마지막 결제 실패 사유 { pgCode, pgMessage }. 결제가 완료되거나 수동 완납으로 정리되면 지워집니다

엔드포인트

GET/invoices/{id}인보이스 조회
규칙

없으면 404입니다.

응답
invoice
POST/invoices수동 청구
요청
{
  "subscriptionId": "sub_001",
  "items": [
    { "productId": "prd_english", "quantity": 1 },
    { "name": "교재비", "unitPrice": 15000, "quantity": 1 }
  ],
  "memo": "9월 교재 추가"
}
규칙
  • subscriptionId와 items가 필수이고 memo는 선택입니다.
  • 항목은 productId + quantity로 보내면 단가를 상품에서 채웁니다. 등록된 상품이 아니면 name + unitPrice + quantity로 직접 적습니다.
  • 상품으로 보낸 항목은 그 상품이 있어야 하고, 청구가 중지된 상품은 담을 수 없습니다.
  • 생성과 동시에 결제합니다.
  • 회차가 없고(sequence는 null) 구독의 회차 번호와 다음 청구 예정일도 바뀌지 않습니다.
  • 구독에 붙은 할인은 자동으로 적용되지 않습니다.
  • 종료된 구독이면 409입니다. 일시중지·미완성 구독에는 청구할 수 있습니다.
응답
invoice
POST/invoices/{id}/retry결제 재시도
규칙
  • 실패한 인보이스에만 쓸 수 있습니다. 아니면 409입니다.
  • 같은 결제 건으로 다시 시도하고 결과를 응답에 담아 돌려줍니다. 이미 결제된 건이면 결제 완료 상태로 맞춥니다.
  • 결제에는 고객의 현재 빌링키를 씁니다. 카드를 새로 등록했다면 새 카드로 시도하며, 빌링키가 없으면 CustomerHasNoBillingKey입니다.
  • 시도하는 동안 인보이스는 PENDING이라 같은 건에 취소·수동 완납을 할 수 없습니다.
  • 첫 회차가 이 요청으로 결제되면 INCOMPLETE였던 구독이 ACTIVE로 돌아갑니다.
응답
invoice
POST/invoices/{id}/cancel인보이스 취소
규칙
  • 실패한 인보이스에만 쓸 수 있습니다. 아니면 409입니다.
  • 취소 상태로 바뀌고, 그 인보이스에 반영됐던 할인의 잔여 회차를 구독에 돌려줍니다.
  • 회차를 다 써서 소진된 할인은 다시 쓸 수 있는 상태가 됩니다.
응답
invoice
POST/invoices/{id}/regenerate인보이스 재생성
규칙
  • 취소된 인보이스에만 쓸 수 있습니다. 아니면 409입니다.
  • 구독의 현재 상태(상품의 현재 가격·할인 등)로 같은 회차의 인보이스를 새로 만들어 즉시 결제합니다.
  • regenerationSeq가 1 늘어나고 원본은 취소 상태로 남습니다.
  • 금액을 직접 받지 않습니다. 금액을 바꾸려면 구독이나 상품·할인을 먼저 고친 뒤 호출해 주세요.
  • 같은 원본으로 두 번 호출하면 InvoiceAlreadyRegenerated입니다.
  • 회차가 없는 수동 청구는 다시 계산할 기준이 없어 대상이 아닙니다. 같은 내용으로 새로 청구해 주세요.
  • 종료된 구독이거나 고객에게 빌링키가 없으면 거절합니다.
응답
invoice
POST/invoices/{id}/settle수동 완납 처리
요청
{ "memo": "9/5 계좌 입금 확인" }
규칙
  • 계좌 입금처럼 API 밖에서 받은 대금을 정리할 때 씁니다. memo는 선택입니다.
  • 실패한 인보이스에만 쓸 수 있습니다. 아니면 409입니다.
  • 수동 완납 상태가 되고 처리 시각이 paidAt에 기록됩니다.
  • 이전 결제 실패 사유는 지워지고 결제 정보(payment)도 내려가지 않습니다.
  • 첫 회차를 이렇게 처리하면 INCOMPLETE였던 구독이 ACTIVE로 돌아갑니다.
응답
invoice

값 목록

구분값
product.pricingTypeRECURRING · ONE_TIME
product.intervalDAILY · DAYS(n) · WEEKLY · MONTHLY · MONTHS(n) · YEARLY
discount.typePERCENT(1~100) · AMOUNT(1 이상, currency 필수)
discount.appliesTo.typeSUBSCRIPTION · PRODUCTS
discount.duration.typeONCE · CYCLES(count 필요) · FOREVER
appliedDiscount.statusACTIVE · EXHAUSTED · REMOVED
customer.statusACTIVE
subscription.statusACTIVE · INCOMPLETE(첫 결제 실패) · PAUSED · ENDED
subscription.endReasonCANCELED · ONE_TIME_EXHAUSTED · END_DATE_REACHED
invoice.statusPENDING(결제 진행 중) · PAID · FAILED · CANCELED · PAID_MANUALLY
invoice.sourceAUTO · MANUAL
결제수단CARD_BILLING (카드 자동이체)

검색어와 일치하는 엔드포인트가 없습니다.