웹훅과 권한 동기화
Paddle 이벤트 저장, 멱등성, 상태 전이, 한국 결제수단 타입 변경 대응
핵심 요약
- 제품 권한은 Checkout 성공이 아니라 서버에서 검증된 webhook 이벤트로만 열어야 합니다.
- 구독 권한의 핵심 이벤트는
subscription.created/updated, 일회성 fulfillment는transaction.completed이며 환불·chargeback에는adjustment.created/updated가 필요합니다. event_id로 비즈니스 이벤트 중복을 막고,notification_id는 전달 추적에 보관하며occurred_at으로 순서를 보정합니다.- 권한은 이벤트마다 if문을 두지 말고 현재 subscription snapshot으로 재계산합니다(active/trialing→full, past_due→grace).
- 한국 결제수단은
south_korea_local_card·kakao_pay등 신규 타입을 쓰되 과거korea_local값도 유지합니다.
Paddle 연동의 안정성은 Checkout 버튼이 아니라 webhook 처리에서 갈립니다. 고객은 결제창을 닫고, 카드 인증은 지연되고, webhook은 순서대로 오지 않습니다. 제품 권한은 반드시 서버에서 검증한 이벤트로만 엽니다.
상품 유형별 핵심 이벤트
Paddle 공식 provisioning 가이드는 구독 권한에 subscription.created와 subscription.updated를,
일회성 상품 fulfillment에는 transaction.completed를 사용합니다. 환불·chargeback·reversal을 구독 상태만으로
표현할 수 없으므로 adjustment 이벤트를 별도로 처리합니다.
| 이벤트 | 목적 |
|---|---|
subscription.created | subscription ID 연결, trialing/active 권한 계산 |
subscription.updated | status, items, next billing date 반영 |
transaction.completed | 일회성 상품 fulfillment, 구독 결제의 재무 상태 연결 |
adjustment.created | refund·chargeback·credit·reversal 요청/생성 기록 |
adjustment.updated | pending approval, approved, rejected 등 최종 상태와 권한 조정 |
운영 분석을 더 촘촘히 하려면 transaction created/updated와 payout 이벤트를 추가하되, 권한 규칙과 재무 원장 규칙을 분리합니다.
저장해야 할 필드
| 필드 | 이유 |
|---|---|
event_id | 동일 Paddle 이벤트의 비즈니스 중복 처리 방지 |
notification_id | destination별 전달·재전송 추적 |
occurred_at | 이벤트 순서 보정 |
paddle_customer_id | 고객 매핑 |
paddle_subscription_id | 구독 조회와 상태 동기화 |
paddle_transaction_id | 결제/환불/대사 연결 |
subscription.status | 권한 상태 결정 |
subscription.consent_requirements | 한국 무료체험·도입 할인 종료 동의 상태 |
subscription.items[].price.id | 플랜/권한 매핑 |
transaction.details.totals | 마지막 결제 금액 표시 |
payments[].method_details.type | 결제수단 분석과 CS |
adjustment.id/action/status | 환불·chargeback·reversal 상태와 권한/원장 연결 |
처리 흐름
멱등성 원칙
1. webhook signature 검증
2. event_id 중복 확인, notification_id 전달 이력 저장
3. 원본 payload 저장
4. occurred_at 기준으로 최신 이벤트인지 확인
5. 내부 billing_state 갱신
6. entitlement 재계산
7. 후속 작업 큐 발행이벤트 핸들러에서 이메일, 권한, CRM, 회계 시스템을 곧바로 다 호출하면 안 됩니다. 원본 이벤트를 저장하고
내부 상태를 갱신한 뒤, 후속 작업은 큐나 job으로 떼어 냅니다. Paddle은 webhook에 Paddle-Signature
헤더를 붙이고, webhook 서버는 200을 빠르게 돌려줘야 합니다. 그래서 raw body 검증과 비동기 처리를 기본값으로
둡니다.
권한 재계산 함수
구독 권한은 이벤트마다 if문으로 흩어 놓지 말고 현재 subscription snapshot으로 계산합니다. 다만 승인된 환불이나 chargeback처럼 subscription status만으로 보이지 않는 재무 사건은 adjustment snapshot을 함께 적용합니다.
function resolveEntitlement(input: {
status: string
priceIds: string[]
nextBilledAt: string | null
scheduledChange?: unknown
revokesEntitlement?: boolean
}) {
if (input.revokesEntitlement) return 'no_paid_access'
if (input.status === 'active' || input.status === 'trialing') return 'full_access'
if (input.status === 'past_due') return 'grace_access'
return 'no_paid_access'
}한국 결제수단 타입
한국 결제수단은 Paddle API와 webhook에 다음처럼 들어올 수 있습니다.
| 타입 | 표시명 |
|---|---|
south_korea_local_card | Korean local card |
kakao_pay | KakaoPay |
naver_pay | Naver Pay |
samsung_pay | Samsung Pay |
payco | Payco |
기존 korea_local 값은 과거 데이터에 그대로 남아 있으니, 분석 파이프라인에서는 historic value를 살려 두고
새 값은 별도 매핑 테이블로 흡수합니다.
장애 대응
| 장애 | 대응 |
|---|---|
| webhook 지연 | Paddle API 재조회로 subscription snapshot 확인 |
| webhook 중복 | event_id로 비즈니스 처리 중복 방지, notification 이력은 보존 |
| 순서 뒤섞임 | occurred_at이 더 오래된 이벤트는 상태 덮어쓰기 금지 |
| 내부 처리 실패 | 원본 payload 재처리 큐 |
| Paddle API 장애 | 권한 상태를 마지막 정상 snapshot 기준으로 임시 유지 |
테이블 설계, raw body signature 검증, replay queue, snapshot reconcile은 Webhook 구현 부록에서 코드 수준으로 다룹니다.
참고 자료
- Paddle Developer - Webhooks
- Paddle Developer - Handle provisioning and fulfillment
- Paddle Developer - subscription.created
- Paddle Developer - adjustment.created
- Paddle Developer - adjustment.updated
- Paddle Developer - Korean subscription consent requirements
- Paddle Developer - Improved Korean payment methods