PayPal Subscription API v1 - 구독 API
개요
PayPal Subscription API를 사용하여 물리적 또는 디지털 상품, 서비스에 대한 반복적인 PayPal 결제를 처리하는 구독을 생성할 수 있습니다. 플랜에는 구독의 금액과 빈도를 정의하는 가격 책정 및 청구 주기 정보가 포함됩니다. $5 기본 플랜과 같은 고정 플랜이나 구매 수량에 따른 가격 책정 계층이 있는 볼륨 또는 단계별 플랜을 정의할 수도 있습니다.
플랜 API (Plans API)
플랜 생성 (Create Plan)
POST /v1/billing/plans
구독에 대한 가격 책정 및 청구 주기 세부 정보를 정의하는 플랜을 생성합니다.
인증
- OAuth2 Bearer 토큰 필요
요청 헤더
헤더 | 타입 | 설명 | 기본값 |
---|---|---|---|
Prefer | string | 성공적인 요청 완료 시 서버 응답 기본 설정 | return=minimal |
PayPal-Request-Id | string | 서버가 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_id | string | 필수 | 카탈로그 제품 API를 통해 생성된 제품 ID (22자) |
name | string | 필수 | 플랜 이름 (1-127자) |
status | string | 선택 | 플랜의 초기 상태 (CREATED, ACTIVE) |
description | string | 선택 | 플랜의 상세 설명 (1-127자) |
billing_cycles | array | 필수 | 청구 주기 배열 (1-12개) |
quantity_supported | boolean | 선택 | 수량 지원 여부 |
payment_preferences | object | 필수 | 결제 기본 설정 |
taxes | object | 선택 | 세금 세부 정보 |
응답 (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_id | string | 제품 ID로 필터링 | - |
page_size | integer | 응답에 반환할 항목 수 (1-20) | 10 |
page | integer | 페이지 번호 (1-100000) | 1 |
total_required | boolean | 총 개수 표시 여부 | 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로 플랜의 세부 정보를 조회합니다.
경로 매개변수
매개변수 | 타입 | 필수 | 설명 |
---|---|---|---|
id | string | 필수 | 플랜의 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 플랜의 경우 상태 업데이트만 가능합니다.
경로 매개변수
매개변수 | 타입 | 필수 | 설명 |
---|---|---|---|
id | string | 필수 | 플랜의 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로 플랜을 활성화합니다.
경로 매개변수
매개변수 | 타입 | 필수 | 설명 |
---|---|---|---|
id | string | 필수 | 플랜의 ID |
응답 (204 No Content)
성공적인 요청은 HTTP 204 No Content 상태 코드를 반환합니다.
플랜 비활성화 (Deactivate Plan)
POST /v1/billing/plans/{id}/deactivate
ID로 플랜을 비활성화합니다.
경로 매개변수
매개변수 | 타입 | 필수 | 설명 |
---|---|---|---|
id | string | 필수 | 플랜의 ID |
응답 (204 No Content)
성공적인 요청은 HTTP 204 No Content 상태 코드를 반환합니다.
가격 책정 업데이트 (Update Pricing)
POST /v1/billing/plans/{id}/update-pricing-schemes
플랜의 가격 책정 스키마를 업데이트합니다.
경로 매개변수
매개변수 | 타입 | 필수 | 설명 |
---|---|---|---|
id | string | 필수 | 플랜의 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_id | string | 필수 | 구독할 플랜의 ID |
start_time | string | 선택 | 구독 시작 시간 (ISO 8601 형식) |
quantity | string | 선택 | 구독 수량 |
shipping_amount | object | 선택 | 배송료 |
subscriber | object | 필수 | 구독자 정보 |
application_context | object | 선택 | 애플리케이션 컨텍스트 |
구독 목록 조회 (List Subscriptions)
GET /v1/billing/subscriptions
구독 목록을 조회합니다.
쿼리 매개변수
매개변수 | 타입 | 설명 | 기본값 |
---|---|---|---|
plan_id | string | 플랜 ID로 필터링 | - |
start_time | string | 시작 시간 | - |
end_time | string | 종료 시간 | - |
page_size | integer | 페이지 크기 (1-20) | 10 |
page | integer | 페이지 번호 | 1 |
total_required | boolean | 총 개수 표시 여부 | false |
구독 상세 정보 조회 (Show Subscription Details)
GET /v1/billing/subscriptions/{id}
ID로 구독의 세부 정보를 조회합니다.
경로 매개변수
매개변수 | 타입 | 필수 | 설명 |
---|---|---|---|
id | string | 필수 | 구독의 ID |
구독 업데이트 (Update Subscription)
PATCH /v1/billing/subscriptions/{id}
구독을 업데이트합니다.
경로 매개변수
매개변수 | 타입 | 필수 | 설명 |
---|---|---|---|
id | string | 필수 | 구독의 ID |
구독 플랜 또는 수량 수정 (Revise Plan or Quantity)
POST /v1/billing/subscriptions/{id}/revise
구독의 플랜 또는 수량을 수정합니다.
경로 매개변수
매개변수 | 타입 | 필수 | 설명 |
---|---|---|---|
id | string | 필수 | 구독의 ID |
구독 일시 정지 (Suspend Subscription)
POST /v1/billing/subscriptions/{id}/suspend
구독을 일시 정지합니다.
경로 매개변수
매개변수 | 타입 | 필수 | 설명 |
---|---|---|---|
id | string | 필수 | 구독의 ID |
요청 본문
{
"reason": "구독자 요청에 의한 일시 정지"
}
구독 취소 (Cancel Subscription)
POST /v1/billing/subscriptions/{id}/cancel
구독을 취소합니다.
경로 매개변수
매개변수 | 타입 | 필수 | 설명 |
---|---|---|---|
id | string | 필수 | 구독의 ID |
요청 본문
{
"reason": "구독자 요청에 의한 취소"
}
구독 활성화 (Activate Subscription)
POST /v1/billing/subscriptions/{id}/activate
구독을 활성화합니다.
경로 매개변수
매개변수 | 타입 | 필수 | 설명 |
---|---|---|---|
id | string | 필수 | 구독의 ID |
요청 본문
{
"reason": "구독자 요청에 의한 활성화"
}
구독 승인된 결제 수집 (Capture Authorized Payment)
POST /v1/billing/subscriptions/{id}/capture
구독에 대해 승인된 결제를 수집합니다.
경로 매개변수
매개변수 | 타입 | 필수 | 설명 |
---|---|---|---|
id | string | 필수 | 구독의 ID |
요청 본문
{
"note": "정기 결제 수집",
"capture_type": "OUTSTANDING_BALANCE",
"amount": {
"currency_code": "USD",
"value": "10.00"
}
}
구독 거래 목록 조회 (List Transactions for Subscription)
GET /v1/billing/subscriptions/{id}/transactions
구독에 대한 거래 목록을 조회합니다.
경로 매개변수
매개변수 | 타입 | 필수 | 설명 |
---|---|---|---|
id | string | 필수 | 구독의 ID |
쿼리 매개변수
매개변수 | 타입 | 설명 |
---|---|---|
start_time | string | 시작 시간 (ISO 8601 형식) |
end_time | string | 종료 시간 (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 구독 생성 플로우
- 제품 생성: 먼저 PayPal Catalog Products API를 사용하여 제품을 생성합니다.
- 플랜 생성: 생성된 제품 ID를 사용하여 구독 플랜을 생성합니다.
- 구독 생성: 플랜 ID를 사용하여 구독을 생성합니다.
- 결제 승인: 사용자가 PayPal을 통해 결제를 승인합니다.
- 구독 활성화: 결제 승인 후 구독이 활성화됩니다.
2.2 구독 관리 플로우
- 구독 상태 모니터링: 웹훅을 통해 구독 상태 변경을 실시간으로 감지합니다.
- 자동 갱신: PayPal이 자동으로 정기 결제를 처리합니다.
- 실패 처리: 결제 실패 시 재시도 로직을 구현합니다.
- 사용자 관리: 사용자가 구독을 일시 정지, 재개, 취소할 수 있는 기능을 제공합니다.
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 구독 서비스를 제공할 수 있습니다. 특히 웹훅을 통한 실시간 상태 동기화와 적절한 오류 처리가 핵심입니다.