Docs
PayPal
구독 API

PayPal Subscription API v1 - 구독 API

개요

PayPal Subscription API를 사용하여 물리적 또는 디지털 상품, 서비스에 대한 반복적인 PayPal 결제를 처리하는 구독을 생성할 수 있습니다. 플랜에는 구독의 금액과 빈도를 정의하는 가격 책정 및 청구 주기 정보가 포함됩니다. $5 기본 플랜과 같은 고정 플랜이나 구매 수량에 따른 가격 책정 계층이 있는 볼륨 또는 단계별 플랜을 정의할 수도 있습니다.

플랜 API (Plans API)

플랜 생성 (Create Plan)

POST /v1/billing/plans

구독에 대한 가격 책정 및 청구 주기 세부 정보를 정의하는 플랜을 생성합니다.

인증

  • OAuth2 Bearer 토큰 필요

요청 헤더

헤더타입설명기본값
Preferstring성공적인 요청 완료 시 서버 응답 기본 설정return=minimal
PayPal-Request-Idstring서버가 72시간 동안 키를 저장-

요청 본문 (Request Body)

{
  "product_id": "PROD-XXCD1234QWER65782",
  "name": "비디오 스트리밍 서비스 플랜",
  "description": "비디오 스트리밍 서비스 기본 플랜",
  "status": "ACTIVE",
  "billing_cycles": [
    {
      "frequency": {
        "interval_unit": "MONTH",
        "interval_count": 1
      },
      "tenure_type": "TRIAL",
      "sequence": 1,
      "total_cycles": 2,
      "pricing_scheme": {
        "fixed_price": {
          "value": "3",
          "currency_code": "USD"
        }
      }
    },
    {
      "frequency": {
        "interval_unit": "MONTH",
        "interval_count": 1
      },
      "tenure_type": "REGULAR",
      "sequence": 2,
      "total_cycles": 12,
      "pricing_scheme": {
        "fixed_price": {
          "value": "10",
          "currency_code": "USD"
        }
      }
    }
  ],
  "payment_preferences": {
    "auto_bill_outstanding": true,
    "setup_fee": {
      "value": "10",
      "currency_code": "USD"
    },
    "setup_fee_failure_action": "CONTINUE",
    "payment_failure_threshold": 3
  },
  "taxes": {
    "percentage": "10",
    "inclusive": false
  }
}

요청 매개변수

매개변수타입필수설명
product_idstring필수카탈로그 제품 API를 통해 생성된 제품 ID (22자)
namestring필수플랜 이름 (1-127자)
statusstring선택플랜의 초기 상태 (CREATED, ACTIVE)
descriptionstring선택플랜의 상세 설명 (1-127자)
billing_cyclesarray필수청구 주기 배열 (1-12개)
quantity_supportedboolean선택수량 지원 여부
payment_preferencesobject필수결제 기본 설정
taxesobject선택세금 세부 정보

응답 (201 Created)

{
  "id": "P-5ML4271244454362WXNWU5NQ",
  "product_id": "PROD-XXCD1234QWER65782",
  "name": "비디오 스트리밍 서비스 플랜",
  "description": "비디오 스트리밍 서비스 기본 플랜",
  "status": "ACTIVE",
  "billing_cycles": [
    {
      "frequency": {
        "interval_unit": "MONTH",
        "interval_count": 1
      },
      "tenure_type": "TRIAL",
      "sequence": 1,
      "total_cycles": 2,
      "pricing_scheme": {
        "fixed_price": {
          "value": "3",
          "currency_code": "USD"
        },
        "version": 1,
        "create_time": "2020-05-27T12:13:51Z",
        "update_time": "2020-05-27T12:13:51Z"
      }
    }
  ],
  "payment_preferences": {
    "auto_bill_outstanding": true,
    "setup_fee": {
      "value": "10",
      "currency_code": "USD"
    },
    "setup_fee_failure_action": "CONTINUE",
    "payment_failure_threshold": 3
  },
  "taxes": {
    "percentage": "10",
    "inclusive": false
  },
  "create_time": "2020-05-27T12:13:51Z",
  "update_time": "2020-05-27T12:13:51Z",
  "links": [
    {
      "href": "https://api-m.paypal.com/v1/billing/plans/P-5ML4271244454362WXNWU5NQ",
      "rel": "self",
      "method": "GET"
    }
  ]
}

플랜 목록 조회 (List Plans)

GET /v1/billing/plans

청구 플랜 목록을 조회합니다.

쿼리 매개변수

매개변수타입설명기본값
product_idstring제품 ID로 필터링-
page_sizeinteger응답에 반환할 항목 수 (1-20)10
pageinteger페이지 번호 (1-100000)1
total_requiredboolean총 개수 표시 여부false

응답 (200 OK)

{
  "plans": [
    {
      "id": "P-9CT60829WM695623HL7QGYOI",
      "name": "넷플릭스 플랜 17012019",
      "status": "ACTIVE",
      "description": "넷플릭스 기본 플랜",
      "usage_type": "LICENSED",
      "create_time": "2020-12-23T07:08:40Z",
      "links": [
        {
          "href": "https://api-m.paypal.com/v1/billing/plans/P-9CT60829WM695623HL7QGYOI",
          "rel": "self",
          "method": "GET"
        }
      ]
    }
  ],
  "links": [
    {
      "href": "https://api-m.paypal.com/v1/billing/plans?page_size=10&page=1",
      "rel": "self",
      "method": "GET"
    }
  ]
}

플랜 상세 정보 조회 (Show Plan Details)

GET /v1/billing/plans/{id}

ID로 플랜의 세부 정보를 조회합니다.

경로 매개변수

매개변수타입필수설명
idstring필수플랜의 ID

응답 (200 OK)

{
  "id": "P-5ML4271244454362WXNWU5NQ",
  "product_id": "PROD-XXCD1234QWER65782",
  "name": "기본 플랜",
  "description": "기본 플랜",
  "status": "ACTIVE",
  "billing_cycles": [
    {
      "frequency": {
        "interval_unit": "MONTH",
        "interval_count": 1
      },
      "tenure_type": "TRIAL",
      "sequence": 1,
      "total_cycles": 2,
      "pricing_scheme": {
        "fixed_price": {
          "currency_code": "USD",
          "value": "3"
        },
        "version": 1,
        "create_time": "2020-05-27T12:13:51Z",
        "update_time": "2020-05-27T12:13:51Z"
      }
    }
  ],
  "taxes": {
    "percentage": "10",
    "inclusive": false
  },
  "create_time": "2020-05-27T12:13:51Z",
  "update_time": "2020-05-27T12:13:51Z",
  "links": [
    {
      "href": "https://api-m.paypal.com/v1/billing/plans/P-5ML4271244454362WXNWU5NQ",
      "rel": "self",
      "method": "GET"
    }
  ]
}

플랜 업데이트 (Update Plan)

PATCH /v1/billing/plans/{id}

CREATED 또는 ACTIVE 상태의 플랜을 업데이트합니다. INACTIVE 플랜의 경우 상태 업데이트만 가능합니다.

경로 매개변수

매개변수타입필수설명
idstring필수플랜의 ID

요청 본문 (JSON Patch)

[
  {
    "op": "replace",
    "path": "/payment_preferences/payment_failure_threshold",
    "value": 7
  },
  {
    "op": "replace",
    "path": "/name",
    "value": "업데이트된 비디오 스트리밍 서비스 플랜"
  }
]

패치 가능한 속성

  • description: 설명 변경
  • payment_preferences.auto_bill_outstanding: 자동 청구 설정
  • taxes.percentage: 세금 비율
  • payment_preferences.payment_failure_threshold: 결제 실패 임계값
  • payment_preferences.setup_fee: 설정 수수료
  • payment_preferences.setup_fee_failure_action: 설정 수수료 실패 시 동작
  • name: 플랜 이름

응답 (204 No Content)

성공적인 요청은 HTTP 204 No Content 상태 코드를 반환하며 JSON 응답 본문은 없습니다.

플랜 활성화 (Activate Plan)

POST /v1/billing/plans/{id}/activate

ID로 플랜을 활성화합니다.

경로 매개변수

매개변수타입필수설명
idstring필수플랜의 ID

응답 (204 No Content)

성공적인 요청은 HTTP 204 No Content 상태 코드를 반환합니다.

플랜 비활성화 (Deactivate Plan)

POST /v1/billing/plans/{id}/deactivate

ID로 플랜을 비활성화합니다.

경로 매개변수

매개변수타입필수설명
idstring필수플랜의 ID

응답 (204 No Content)

성공적인 요청은 HTTP 204 No Content 상태 코드를 반환합니다.

가격 책정 업데이트 (Update Pricing)

POST /v1/billing/plans/{id}/update-pricing-schemes

플랜의 가격 책정 스키마를 업데이트합니다.

경로 매개변수

매개변수타입필수설명
idstring필수플랜의 ID

구독 API (Subscriptions API)

구독 생성 (Create Subscription)

POST /v1/billing/subscriptions

구독을 생성합니다.

요청 본문

{
  "plan_id": "P-5ML4271244454362WXNWU5NQ",
  "start_time": "2025-01-01T00:00:00Z",
  "quantity": "1",
  "shipping_amount": {
    "currency_code": "USD",
    "value": "10.00"
  },
  "subscriber": {
    "name": {
      "given_name": "홍",
      "surname": "길동"
    },
    "email_address": "subscriber@example.com"
  },
  "application_context": {
    "brand_name": "우리 회사",
    "locale": "ko-KR",
    "shipping_preference": "SET_PROVIDED_ADDRESS",
    "user_action": "SUBSCRIBE_NOW",
    "payment_method": {
      "payer_selected": "PAYPAL",
      "payee_preferred": "IMMEDIATE_PAYMENT_REQUIRED"
    },
    "return_url": "https://example.com/returnUrl",
    "cancel_url": "https://example.com/cancelUrl"
  }
}

요청 매개변수

매개변수타입필수설명
plan_idstring필수구독할 플랜의 ID
start_timestring선택구독 시작 시간 (ISO 8601 형식)
quantitystring선택구독 수량
shipping_amountobject선택배송료
subscriberobject필수구독자 정보
application_contextobject선택애플리케이션 컨텍스트

구독 목록 조회 (List Subscriptions)

GET /v1/billing/subscriptions

구독 목록을 조회합니다.

쿼리 매개변수

매개변수타입설명기본값
plan_idstring플랜 ID로 필터링-
start_timestring시작 시간-
end_timestring종료 시간-
page_sizeinteger페이지 크기 (1-20)10
pageinteger페이지 번호1
total_requiredboolean총 개수 표시 여부false

구독 상세 정보 조회 (Show Subscription Details)

GET /v1/billing/subscriptions/{id}

ID로 구독의 세부 정보를 조회합니다.

경로 매개변수

매개변수타입필수설명
idstring필수구독의 ID

구독 업데이트 (Update Subscription)

PATCH /v1/billing/subscriptions/{id}

구독을 업데이트합니다.

경로 매개변수

매개변수타입필수설명
idstring필수구독의 ID

구독 플랜 또는 수량 수정 (Revise Plan or Quantity)

POST /v1/billing/subscriptions/{id}/revise

구독의 플랜 또는 수량을 수정합니다.

경로 매개변수

매개변수타입필수설명
idstring필수구독의 ID

구독 일시 정지 (Suspend Subscription)

POST /v1/billing/subscriptions/{id}/suspend

구독을 일시 정지합니다.

경로 매개변수

매개변수타입필수설명
idstring필수구독의 ID

요청 본문

{
  "reason": "구독자 요청에 의한 일시 정지"
}

구독 취소 (Cancel Subscription)

POST /v1/billing/subscriptions/{id}/cancel

구독을 취소합니다.

경로 매개변수

매개변수타입필수설명
idstring필수구독의 ID

요청 본문

{
  "reason": "구독자 요청에 의한 취소"
}

구독 활성화 (Activate Subscription)

POST /v1/billing/subscriptions/{id}/activate

구독을 활성화합니다.

경로 매개변수

매개변수타입필수설명
idstring필수구독의 ID

요청 본문

{
  "reason": "구독자 요청에 의한 활성화"
}

구독 승인된 결제 수집 (Capture Authorized Payment)

POST /v1/billing/subscriptions/{id}/capture

구독에 대해 승인된 결제를 수집합니다.

경로 매개변수

매개변수타입필수설명
idstring필수구독의 ID

요청 본문

{
  "note": "정기 결제 수집",
  "capture_type": "OUTSTANDING_BALANCE",
  "amount": {
    "currency_code": "USD",
    "value": "10.00"
  }
}

구독 거래 목록 조회 (List Transactions for Subscription)

GET /v1/billing/subscriptions/{id}/transactions

구독에 대한 거래 목록을 조회합니다.

경로 매개변수

매개변수타입필수설명
idstring필수구독의 ID

쿼리 매개변수

매개변수타입설명
start_timestring시작 시간 (ISO 8601 형식)
end_timestring종료 시간 (ISO 8601 형식)

구독 상태 (Subscription Status)

구독은 다음과 같은 상태를 가질 수 있습니다:

  • APPROVAL_PENDING: 승인 대기 중
  • APPROVED: 승인됨
  • ACTIVE: 활성화됨
  • SUSPENDED: 일시 정지됨
  • CANCELLED: 취소됨
  • EXPIRED: 만료됨

청구 주기 타입 (Billing Cycle Types)

  • TRIAL: 평가판 기간
  • REGULAR: 정기 청구
  • INFINITE: 무한 청구 (종료 날짜 없음)

간격 단위 (Interval Units)

  • DAY: 일
  • WEEK: 주
  • MONTH: 월
  • YEAR: 년

구독 시스템 구축 방안

1. 아키텍처 설계

PayPal 구독 시스템을 구축할 때 다음과 같은 아키텍처를 권장합니다:

Frontend (React/Vue/Angular)

Backend API (Node.js/Python/Java)

Database (PostgreSQL/MySQL)

PayPal Subscription API

Webhook Handler

2. 기본 플로우

2.1 구독 생성 플로우

  1. 제품 생성: 먼저 PayPal Catalog Products API를 사용하여 제품을 생성합니다.
  2. 플랜 생성: 생성된 제품 ID를 사용하여 구독 플랜을 생성합니다.
  3. 구독 생성: 플랜 ID를 사용하여 구독을 생성합니다.
  4. 결제 승인: 사용자가 PayPal을 통해 결제를 승인합니다.
  5. 구독 활성화: 결제 승인 후 구독이 활성화됩니다.

2.2 구독 관리 플로우

  1. 구독 상태 모니터링: 웹훅을 통해 구독 상태 변경을 실시간으로 감지합니다.
  2. 자동 갱신: PayPal이 자동으로 정기 결제를 처리합니다.
  3. 실패 처리: 결제 실패 시 재시도 로직을 구현합니다.
  4. 사용자 관리: 사용자가 구독을 일시 정지, 재개, 취소할 수 있는 기능을 제공합니다.

3. 데이터베이스 스키마 설계

-- 제품 테이블
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    paypal_product_id VARCHAR(50) UNIQUE NOT NULL,
    name VARCHAR(127) NOT NULL,
    description TEXT,
    category VARCHAR(50),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
 
-- 플랜 테이블
CREATE TABLE plans (
    id SERIAL PRIMARY KEY,
    paypal_plan_id VARCHAR(50) UNIQUE NOT NULL,
    product_id INTEGER REFERENCES products(id),
    name VARCHAR(127) NOT NULL,
    description TEXT,
    status VARCHAR(20) DEFAULT 'ACTIVE',
    billing_cycles JSONB,
    payment_preferences JSONB,
    taxes JSONB,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
 
-- 구독 테이블
CREATE TABLE subscriptions (
    id SERIAL PRIMARY KEY,
    paypal_subscription_id VARCHAR(50) UNIQUE NOT NULL,
    plan_id INTEGER REFERENCES plans(id),
    user_id INTEGER REFERENCES users(id),
    status VARCHAR(20) DEFAULT 'APPROVAL_PENDING',
    start_time TIMESTAMP,
    next_billing_time TIMESTAMP,
    quantity INTEGER DEFAULT 1,
    shipping_amount DECIMAL(10,2),
    subscriber_info JSONB,
    application_context JSONB,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
 
-- 구독 거래 테이블
CREATE TABLE subscription_transactions (
    id SERIAL PRIMARY KEY,
    subscription_id INTEGER REFERENCES subscriptions(id),
    paypal_transaction_id VARCHAR(50) UNIQUE NOT NULL,
    amount DECIMAL(10,2),
    currency_code VARCHAR(3),
    transaction_type VARCHAR(20), -- PAYMENT, REFUND, CANCELLATION
    status VARCHAR(20),
    transaction_time TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

4. 백엔드 구현 예시 (Node.js)

const express = require('express');
const paypal = require('@paypal/checkout-server-sdk');
 
// PayPal 환경 설정
const environment = new paypal.core.SandboxEnvironment(
    process.env.PAYPAL_CLIENT_ID,
    process.env.PAYPAL_CLIENT_SECRET
);
const client = new paypal.core.PayPalHttpClient(environment);
 
// 플랜 생성
app.post('/api/plans', async (req, res) => {
    try {
        const request = new paypal.orders.OrdersCreateRequest();
        request.prefer("return=representation");
        request.requestBody({
            product_id: req.body.product_id,
            name: req.body.name,
            description: req.body.description,
            status: "ACTIVE",
            billing_cycles: req.body.billing_cycles,
            payment_preferences: req.body.payment_preferences,
            taxes: req.body.taxes
        });
 
        const response = await client.execute(request);
        
        // 데이터베이스에 플랜 정보 저장
        await savePlanToDatabase(response.result);
        
        res.json(response.result);
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});
 
// 구독 생성
app.post('/api/subscriptions', async (req, res) => {
    try {
        const request = new paypal.orders.OrdersCreateRequest();
        request.prefer("return=representation");
        request.requestBody({
            plan_id: req.body.plan_id,
            start_time: req.body.start_time,
            quantity: req.body.quantity,
            subscriber: req.body.subscriber,
            application_context: req.body.application_context
        });
 
        const response = await client.execute(request);
        
        // 데이터베이스에 구독 정보 저장
        await saveSubscriptionToDatabase(response.result);
        
        res.json(response.result);
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});
 
// 웹훅 처리
app.post('/api/webhooks/paypal', async (req, res) => {
    const event = req.body;
    
    switch (event.event_type) {
        case 'BILLING.SUBSCRIPTION.CREATED':
            await handleSubscriptionCreated(event);
            break;
        case 'BILLING.SUBSCRIPTION.ACTIVATED':
            await handleSubscriptionActivated(event);
            break;
        case 'BILLING.SUBSCRIPTION.SUSPENDED':
            await handleSubscriptionSuspended(event);
            break;
        case 'BILLING.SUBSCRIPTION.CANCELLED':
            await handleSubscriptionCancelled(event);
            break;
        case 'PAYMENT.SALE.COMPLETED':
            await handlePaymentCompleted(event);
            break;
        case 'PAYMENT.SALE.DENIED':
            await handlePaymentFailed(event);
            break;
        default:
            console.log('Unhandled webhook event:', event.event_type);
    }
    
    res.status(200).send('OK');
});

5. 프론트엔드 구현 예시 (React)

import React, { useState, useEffect } from 'react';
import { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js';
 
const SubscriptionComponent = ({ planId }) => {
    const [subscriptionID, setSubscriptionID] = useState('');
    const [isLoading, setIsLoading] = useState(false);
 
    const createSubscription = (data, actions) => {
        return actions.subscription.create({
            plan_id: planId,
            application_context: {
                brand_name: "우리 회사",
                locale: "ko-KR",
                shipping_preference: "NO_SHIPPING",
                user_action: "SUBSCRIBE_NOW",
                return_url: "https://example.com/return",
                cancel_url: "https://example.com/cancel"
            }
        });
    };
 
    const onApprove = (data, actions) => {
        setSubscriptionID(data.subscriptionID);
        
        // 백엔드에 구독 정보 저장
        fetch('/api/subscriptions/activate', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                subscriptionID: data.subscriptionID
            })
        })
        .then(response => response.json())
        .then(data => {
            alert('구독이 성공적으로 생성되었습니다!');
        })
        .catch(error => {
            console.error('Error:', error);
            alert('구독 생성 중 오류가 발생했습니다.');
        });
    };
 
    const onError = (err) => {
        console.error('PayPal Error:', err);
        alert('구독 생성 중 오류가 발생했습니다.');
    };
 
    return (
        <PayPalScriptProvider options={{
            "client-id": process.env.REACT_APP_PAYPAL_CLIENT_ID,
            vault: true,
            intent: "subscription"
        }}>
            <div className="subscription-container">
                <h2>구독 플랜 선택</h2>
                <PayPalButtons
                    createSubscription={createSubscription}
                    onApprove={onApprove}
                    onError={onError}
                    style={{
                        layout: "vertical",
                        color: "blue",
                        shape: "rect",
                        label: "subscribe"
                    }}
                />
                {subscriptionID && (
                    <div className="subscription-success">
                        <p>구독 ID: {subscriptionID}</p>
                        <p>구독이 성공적으로 생성되었습니다!</p>
                    </div>
                )}
            </div>
        </PayPalScriptProvider>
    );
};
 
export default SubscriptionComponent;

6. 구독 관리 대시보드

구독 시스템에는 다음과 같은 관리 기능이 필요합니다:

6.1 사용자 대시보드

  • 구독 상태 확인
  • 구독 일시 정지/재개
  • 구독 취소
  • 결제 내역 조회
  • 플랜 변경

6.2 관리자 대시보드

  • 전체 구독 통계
  • 구독 수익 분석
  • 구독 실패 건 관리
  • 사용자 구독 관리
  • 플랜 관리

7. 모니터링 및 알림

7.1 모니터링 항목

  • 구독 생성 성공률
  • 결제 실패율
  • 구독 취소율
  • 월별 반복 수익 (MRR)
  • 고객 생애 가치 (CLV)

7.2 알림 설정

  • 결제 실패 알림
  • 구독 취소 알림
  • 높은 실패율 알림
  • 시스템 오류 알림

8. 보안 고려사항

8.1 API 보안

  • OAuth 2.0 토큰 관리
  • API 키 보안 저장
  • HTTPS 사용 필수
  • 웹훅 서명 검증

8.2 데이터 보안

  • 개인정보 암호화
  • 결제 정보 비저장
  • 로그 관리
  • 접근 권한 제어

9. 테스트 전략

9.1 단위 테스트

  • API 엔드포인트 테스트
  • 웹훅 처리 테스트
  • 데이터베이스 연산 테스트

9.2 통합 테스트

  • PayPal API 연동 테스트
  • 전체 구독 플로우 테스트
  • 오류 시나리오 테스트

9.3 샌드박스 테스트

  • PayPal 샌드박스 환경 사용
  • 다양한 결제 시나리오 테스트
  • 웹훅 이벤트 테스트

10. 배포 및 운영

10.1 배포 전략

  • 단계별 배포 (스테이징 → 프로덕션)
  • 블루-그린 배포
  • 롤백 계획

10.2 운영 모니터링

  • 시스템 가용성 모니터링
  • 성능 모니터링
  • 오류 추적 및 알림
  • 사용자 행동 분석

이러한 구독 시스템을 구축하면 안정적이고 확장 가능한 PayPal 구독 서비스를 제공할 수 있습니다. 특히 웹훅을 통한 실시간 상태 동기화와 적절한 오류 처리가 핵심입니다.