Skip to main content
구독은 고객이 공급자의 상품, 서비스 또는 컨텐츠에 지속적으로 접근하기 위해 반복 결제에 동의하는 공급자와 소비자간의 계약입니다. 구독자가 특정 금액이 월간, 연간 등의 일정 주기로 결제하는 것에 동의하게 되면 공급자는 이후 사용자의 인터렉션 없이 등록된 결제 수단에서 결제를 발생할 수 있게 됩니다. 스텝페이는 공급자와 구독 계약을 체결하고 반복 결제를 발생하기 위해 필요한 모든 과정을 SaaS 형태로 제공하고 있습니다. subscription_flow.png 스텝페이에서 구독(Subscription) 이란 일정 주기로 가격이 결정된 RECURRING 타입의 주문(Order) 을 생성하는 엔티티입니다. 구독(Subscription)은 RECURRING_INITIAL 타입의 주문(Order)이 결제되면 자동으로 생성되며 이후 주기적으로 RECURRING 타입의 주문(Order)을 생성하기 때문에 RECURRING_INITIAL 타입의 주문은 구독과 일대다 관계를 가짐과 동시에, 구독은 RECURRING 타입의 주문과 일대다 관계를 가집니다. RECURRING_INITIAL 타입의 주문(Order) 을 결제할 때 고객이 사용한 결제수단이 구독(Subscription) 의 결제수단으로 등록되며, 갱신 결제에 해당 결제수단이 이용됩니다. 결제 수단 변경을 하려면 고객이 변경하고자 하는 결제수단으로 다시 결제를 진행해야 합니다.

연관 가이드

사전 준비 작업

  • 구독 가격 플랜 담긴 주문이 생성되어 있어야 합니다.
  • 정기 결제 가능한 PG 가 연동되어 있어야 합니다.

구독의 생성

FLAT, BUNDLE, USAGE_BASED, UNIT_BASED 타입의 가격 플랜이 포함된 주문을 결제하면 구독이 자동으로 생성됩니다.
구독은 고객과의 계약이기 때문에 API 를 통해 생성할 수 없으며, 최초 1번 고객이 수동으로 결제하는 방법으로만 생성할 수 있습니다.
구독의 주기는 가격 플랜의 속성과 상품의 무료 체험과 같은 일부 속성들에 의해 결정됩니다.
만약 여러 개의 주문 아이템이 포함되어 있는 RECURRING_INITIAL 주문이 결제되면 구독 묶음을 결정하는 요소에 의해 구독이 여러개 생길 수 있습니다. 다음은 주문 아이템들이 구독 묶음으로 결정되는 속성들입니다.
필드설명
Current Period End Date현재 결제 주기의 종료 날짜 (예: 첫 번째 주기 종료 날짜)
Interval구독 주기 (예: 1개월)
Expiry Date서비스나 구독이 만료되는 날짜 (특정 날짜에 만료되는 경우)
Provide Start Day주기 내에서 서비스 제공이 시작되는 날 (해당하는 경우)
Billing Date결제 주기와 관계없이 매월 특정한 결제일 (예: 매월 5일 결제)
Trial End무료 체험 기간이 끝나는 날짜
Expiry Recurring Count만료되기 전까지 허용된 최대 갱신 횟수 (예: 10번 갱신 후 만료)
Claim Method Type결제 방식 (선불 또는 후불)

구독의 생명주기

구독의 생명주기는 구독의 상태가 어떻게 변하는지를 나타냅니다. 각 상태는 특정 이벤트에 의해 변경될 수 있습니다. 구독의 생명주기는 스텝페이에서 자동으로 관리되지만 연동 개발하고 있는 시스템과 통합하려면 구독의 상태 변경과 시스템 상태를 동기화 해야합니다. 예를들면, 구독이 일시정지 되었을 때 해당 고객의 SaaS 소프트웨어의 사용이 중지되어야 할 수 있습니다. 시스템과 통합하는 자세한 방법은 연동 가이드를 참고하세요. 다음은 구독의 상태 및 이벤트에 대한 개요입니다.

구독 상태

  • ACTIVE: 구독이 활성화 상태이며, 사용자가 서비스를 이용할 수 있는 상태입니다.
  • UNPAID: 스케쥴 된 주문이 결제 실패하여 서비스 이용이 일시 중지된 상태입니다. 구독이 이 상태가 되면 복구 프로세스가 동작하게 됩니다.
  • PENDING_CANCEL: 사용자가 구독 취소를 요청했으나, 아직 최종 취소 상태로 전환되지 않은 상태입니다. 구독 취소 API 또는 스텝페이 포탈에서 구독 취소 옵션으로 특정일 취소 또는 현제 주기 종료 후 취소 옵션을 선택하면 구독이 이 상태로 변경됩니다.
  • CANCELED: 구독이 최종적으로 취소된 상태입니다. 구독 취소 API 또는 스텝페이 포탈에서 구독 취소 옵션으로 즉시 취소 옵션을 선택하거나 취소 요청일이 되면 구독이 이 상태로 변경됩니다. 취소된 구독은 다시 복구될 수 없습니다.
  • PENDING_PAUSE: 사용자가 구독 일시정지를 요청했으나, 아직 최종 일시정지 상태로 전환되지 않은 상태입니다. 구독 일시정지 API 또는 스텝페이 포탈에서 구독 일시정지 옵션으로 특정일 일시정지 또는 현제 주기 종료 후 일시정지 옵션을 선택하면 구독이 이 상태로 변경됩니다.
  • PAUSE: 구독이 일시 중지된 상태입니다.
  • EXPIRED: 복구 프로세스 에도 구독 복구를 실패하거나 만료일을 지정한 구독의 경우 만료일이 도래하면 구독이 만료된 상태입니다. 만료된 구독은 다시 복구될 수 없습니다.
취소 또는 만료된 구독은 다시 복구될 수 없습니다.
👉 구독 상태 다이어그램 을 확인해보세요.

주문 취소가 구독에 미치는 영향

구독을 생성한 주문 (RECURRING_INITIAL) 이 취소되면 해당 주문에 의해 생성된 모든 구독이 취소됩니다. 하지만 구독이 취소되어도 해당 구독과 관련된 주문이 환불되는 것은 아닙니다. 구독 취소와 함께 이미 결제된 주문을 취소하기 위해서는 별도로 주문 취소 API를 사용해야 합니다. 이와 반대로 구독에 의해 생성된 주문(RECURRING) 은 환불 혹은 취소되어도 구독은 취소되지 않습니다. 주문 환불과 함께 구독 또한 취소 하려면 별도로 구독 취소 API를 사용해야 합니다.

선불 구독과 후불 구독

선불 구독과 후불 구독은 주기적으로 RECURRING 주문을 생성한다는 점에서는 동일하지만 동작 방식에 약간의 차이가 있습니다. 선불 구독은 만료되거나 취소될 때 추가적인 결제가 일어나지 않지만 후불 구독의 경우 마지막 RECURRING 주문을 생성한 뒤 취소 혹은 만료됩니다. 또 한가지 다른 점은 갱신 횟수를 세는 방식입니다. 구독 상세 API를 통해 알아낼 수 있는 갱신 횟수(recurringCount)가 구독 최초 생성 시 선불 구독의 경우 1, 후불 구독의 경우 0으로 시작됩니다.

갱신 실패에 대한 재시도 정책

카드 유효기간 만료, 잔액 부족 등의 사유로 갱신 결제가 실패한 경우 복구 프로세스가 시작됩니다. 구독이 결제 실패되면 구독은 그 즉시 UNPAID 상태가 됩니다. 기본적으로 복구 프로세스는 최초 결제 실패 일로부터 1일, 3일, 5일, 10일, 14일 후에 결제를 재시도 하는 정책으로 동작합니다. 복구 프로세스를 통해 구독이 복구되면 구독의 상태는 ACTIVE 상태로 돌아옵니다. 연동하는 시스템은 구독의 상태를 웹훅을 통해 관찰하고 있어야 하며 구독을 복구하기 위해 고객의 결제 수단을 변경하는 방법을 제공해야 할 수도 있습니다.

결제 수단 변경

구독은 내부적으로 PG에서 발급되는 빌링키를 통해 결제됩니다. 자동으로 갱신되는 결제에 대한 결제 수단을 변경하기 위해서는 새로운 빌링키 발급이 필요합니다. 스텝페이서는 PAYMENT_METHOD 타입의 주문을 통해 빌링키 발급이 이루어집니다. 결제수단 변경 API를 이용하여 PAYMENT_METHOD 타입의 주문을 생성할 수 있고 새로운 결제 수단을 등록하기 위해서는 고객의 결제 정보를 받아야 하므로 결제 링크를 생성하여 고객을 결제 페이지로 리다이렉션 해야합니다. 고객이 결제 정보를 입력하면 구독의 결제 수단이 업데이트 됩니다.

연동 가이드

스텝페이 구독의 생명주기를 연동 개발하고자 하는 시스템(이하 시스템)과 동기화 하기 위해서 시스템은 스텝페이 구독번호를 시스템의 결제 주체에 맵핑해야 합니다. 구독은 주문이 결제되었을 때 생성되므로 시스템은 먼저 주문 생성 API를 통해 얻을수 있는 주문 번호를 저장한 뒤 결제 성공 리디렉션을 처리해야 합니다.

결제 링크 생성 후 주문 코드 확인

스텝페이 API 로 주문을 생성하고 결제 주체(Organization)에 주문 번호를 맵핑한 뒤 결제 링크로 고객을 리디렉트합니다. 주문이 완료되면 구독이 생성됩니다. 구독 번호는 결제 성공 리디렉션에서 주문 코드를 통해 확인할 수 있습니다.
async generateLink(name: string, plan: Plan, userId: number): Promise<string> {
    const user = await this.userService.getUser(userId)

    // Retrieves the StepPay product/price code corresponding to the plan selected by the customer.
    let planItem = this.PLAN[plan]

    // StepPay order creation
    const order = await this.steppayService.createOrder(
            user.steppayId,
            planItem, // { productCode: string, priceCode: string }[]
    )

    // Mapping of the payment entity in the system and the StepPay order number
    await this.organizationRepository.save({
        name,
        plan,
        owner: user,
        steppayOrderId: order.data.orderId
    })

    return this.steppayService.generateLink(order.data.orderId) // redirection
}

결제 완료 리디렉션 처리

// Payment completion redirection
async processPayment(order: SteppayOrder): Promise<OrganizationEntity> {
    // Find the payment entity by the redirected order number
    const organization = await this.organizationRepository.findOne({ where: { steppayOrderId: order.orderId } })
    if (!organization) {
        throw new NotFoundException()
    }

    // Map all subscription numbers created by the order
    organization.subscriptions = order.subscriptions.map(subscription => new SubscriptionEntity(
            organization,
            subscription.id,
    ))

    // Retrieve subscription item number and product code for usage tracking
    const items = order.subscriptions.flatMap(subscription => subscription.items)
            .map(item => ({ id: item.id, productCode: item.productCode }))

    // Map the detailed metering items of the payment entity and the StepPay subscription item number
    organization.items = new ItemEmbedable(
            items.find(items => items.productCode === this.PRODUCT_CODE).id
    )

    organization.status = 'ACTIVE'

    await this.organizationRepository.save(organization)

    return organization
}

구독 웹훅

스텝페이 구독과 결제 주체의 맵핑이 완료되었으면 이제 구독 웹훅을 통해 상태를 동기화 해야 합니다. 스텝페이 구독의 상태 변경에 따른 처리는 시스템의 고객 정책에 따라 달라질 수 있습니다. 웹훅에 대한 자세한 내용은 웹훅 가이드를 참고하세요.
@Get()
async
handleSubscriptionWebhook(
    @Body() body: {
      id: number,
      status : string
    }
) {
    this.organizationService.updateStatus(body)
}