개요
주문형 구독을 사용하면 고객의 결제 수단을 한 번 승인한 후, 고정된 일정 대신 필요할 때마다 변동 금액을 청구할 수 있습니다. 이 기능은 모든 계정에서 사용할 수 있으며 별도의 승인이 필요하지 않습니다. 이 가이드를 사용하여 다음을 수행할 수 있습니다:- 주문형 구독 생성(선택적 초기 가격과 함께 mandate 승인)
- 사용자 지정 금액으로 후속 청구 실행
- webhook을 사용하여 결과 추적
사전 요구 사항
- Dodo Payments merchant 계정 및 API key
- webhook secret이 구성되어 있고 이벤트를 수신할 endpoint
- 카탈로그에 등록된 구독 상품
주문형 작동 방식
on_demand객체를 사용하여 구독을 생성하고 결제 수단을 승인하며, 선택적으로 초기 청구 금액을 수금합니다.- 이후 전용 charge endpoint를 사용하여 사용자 지정 금액으로 해당 구독에 대한 청구를 생성합니다.
- webhook(예:
payment.succeeded,payment.failed)을 수신하여 시스템을 업데이트합니다.
주문형 구독 생성
Endpoint: POST /checkouts 주요 request fields (body):Create Checkout Session에서 확인하세요.
주문형 구독 생성
- Node.js SDK
- Python SDK
- Go SDK
- cURL
Success
주문형 구독 청구
mandate가 승인되면 필요에 따라 청구를 생성합니다. Endpoint: POST /subscriptions/{subscription_id}/charge 주요 request fields (body):- Node.js SDK
- Python SDK
- Go SDK
- cURL
Success
실패한 청구 처리
주문형 구독에 대한 청구가 실패하면 다음에 수행할 작업을 직접 결정합니다. 예약된 구독에서는 실패한 갱신으로 인해 이후 자동 결제가 중지되지만, 주문형 구독은 실패 후에도 청구할 수 있습니다. 자체 재시도 로직의 일부로 charge endpoint를 다시 호출할 수 있습니다.실패 시 동작
1
Charge attempt fails
POST /subscriptions/{subscription_id}/charge request는 오류 응답을 반환하거나 비동기적으로 완료된 후 거절 사유가 포함된 payment.failed webhook을 발생시킵니다.2
Subscription may transition to on_hold
구독이
on_hold 상태로 전환되고 subscription.on_hold webhook을 발생시킬 수 있습니다(Subscription States → On Hold 참조). 이는 신호일 뿐 잠금이 아닙니다. 주문형 구독에서는 on_hold 상태여도 다시 청구할 수 있습니다.3
Retry the charge (your call)
주문형 flow에서는 Dodo가 자동으로 재시도하지 않습니다. 언제든
POST /subscriptions/{subscription_id}/charge를 다시 호출하여 재시도할 수 있습니다. 아래의 안전한 재시도 정책을 적용하세요. 지수 백오프를 사용하고, 영구 거절을 건너뛰며, 일괄 재시도 패턴을 피해야 당사의 fraud 및 risk 시스템에 의해 플래그되지 않습니다.4
Optionally, ask the customer for a new payment method
결제 수단 자체의 문제(만료된 카드, 폐쇄된 계정 등)로 인해 재시도가 계속 실패하면
POST /subscriptions/{subscription_id}/update-payment-method를 사용하여 고객으로부터 새 결제 수단을 수집하세요. 성공하면 구독이 active로 돌아가고 payment.succeeded에 이어 subscription.active webhook이 발생합니다.주문형과 예약형 비교: 예약된 구독에서는 Dodo가 자체 갱신 재시도와 dunning을 실행합니다. 주문형 구독에서는 다음 청구가 언제 발생해야 하는지 사용자만 알고 있으므로 재시도 정책을 직접 관리해야 합니다(달력이 아니라 사용량 이벤트에 의해 결정됨).
주문형 청구 실패 시 webhook 순서
3번과 4번 이벤트는 후속 청구가 성공한 후에만 발생합니다.
재시도 책임
Subscription Dunning(내장된 이메일 복구 sequence)은 예약된 구독의 실패한 갱신 결제와 고객이 시작한 취소에 적용됩니다. 주문형 청구 실패를 위한 기능이 아닙니다. 결제 수단을 업데이트해야 한다고 판단되면 고객에게 직접(예: transactional email 또는 인앱 prompt) 안내하세요.결제 재시도
당사의 fraud detection 시스템은 공격적인 재시도 패턴을 차단할 수 있으며 이를 잠재적인 card testing으로 플래그할 수 있습니다. 안전한 재시도 정책을 따르세요.안전한 재시도 정책의 원칙
- 백오프 메커니즘: 재시도 사이에 지수 백오프를 사용합니다.
- 재시도 한도: 총 재시도 횟수를 제한합니다(최대 3~4회).
- 지능형 필터링: 재시도 가능한 실패(예: network/issuer 오류, 잔액 부족)에만 재시도하고, 영구 거절은 절대 재시도하지 않습니다.
- Card testing 방지:
DO_NOT_HONOR,STOLEN_CARD,LOST_CARD,PICKUP_CARD,FRAUDULENT,AUTHENTICATION_FAILURE와 같은 실패는 재시도하지 않습니다. - metadata 변경(선택 사항): 자체 재시도 시스템을 운영하는 경우 metadata(예:
retry_attempt)를 통해 재시도를 구분합니다.
권장 재시도 일정(구독)
- 1차 시도: 청구 생성 시 즉시
- 2차 시도: 3일 후
- 3차 시도: 추가 7일 후(총 10일)
- 4차 시도(최종): 다시 7일 후(총 17일)
일괄 재시도를 피하고 승인 시간에 맞추기
- 전체 포트폴리오에서 “일괄” 동작이 발생하지 않도록 재시도를 최초 승인 timestamp에 맞춥니다.
- 예: 고객이 오늘 오후 1시 10분에 trial 또는 mandate를 시작했다면 백오프에 따라 이후 날짜의 오후 1시 10분에 후속 재시도를 예약합니다(예: +3일 → 오후 1시 10분, +7일 → 오후 1시 10분).
- 또는 마지막 성공 결제 시간을
T에 저장하는 경우, 시간대 정렬을 유지하도록T + X days에 다음 시도를 예약합니다.
Time-zone 및 DST: 일정을 예약할 때 일관된 시간 표준을 사용하고, 간격을 유지하기 위해 표시할 때만 변환합니다.
재시도하지 않아야 하는 거절 코드
STOLEN_CARDDO_NOT_HONORFRAUDULENTPICKUP_CARDAUTHENTICATION_FAILURELOST_CARD
거절 사유의 전체 목록과 사용자가 수정할 수 있는 사유인지 여부는
Transaction Failures 문서를 참조하세요.
구현 지침(코드 없음)
- 정확한 timestamp를 유지하는 scheduler/queue를 사용하고, 정확한 시간대 오프셋에 다음 시도를 계산합니다(예: 동일한 HH:MM의
T + 3 days). - 마지막 성공 결제 timestamp
T를 유지하고 참조하여 다음 시도를 계산합니다. 여러 구독을 동일한 순간에 몰아서 처리하지 마세요. - 항상 마지막 거절 사유를 평가하고, 위의 건너뛰기 목록에 있는 영구 거절에 대해서는 재시도를 중지합니다.
- 고객 및 계정별 동시 재시도 수를 제한하여 우발적인 급증을 방지합니다.
- 사전에 안내하세요. 다음 예약 시도 전에 고객에게 email/SMS로 결제 수단 업데이트를 요청합니다.
- metadata는 관찰 가능성 확보 용도로만 사용하고(예:
retry_attempt), 중요하지 않은 필드를 변경하여 fraud/risk 시스템을 “회피”하려고 하지 마세요.
취소
주문형 구독은 즉시 종료 시점을 고정된 billing cycle에 맞출 수 없기 때문에 예약된 구독과 다른 취소 flow를 따릅니다.Customer Portal 동작
고객이 Customer Portal에서 주문형 구독을 취소하면 기본적으로 취소가 다음 billing date에 예약됩니다. Cancel Now 옵션은 주문형 구독에 의도적으로 표시되지 않습니다. 그 이유는 주문형 구독에 예측 가능한 반복 갱신 날짜가 없기 때문입니다. 다음 청구 시간은 전적으로 사용량 이벤트에 의해 결정됩니다. 다음 billing date에 취소를 예약하면 period boundary까지 mandate가 활성 상태로 유지되어 진행 중인 사용량을 계속 청구할 수 있고, 이후 구독이 정상적으로 종료됩니다. 고객이 취소를 확인한 후:- 구독은
active상태를 유지하며 예약된 취소 날짜까지POST /subscriptions/{id}/charge를 통해 계속 청구할 수 있습니다. - 구독의
cancel_at_next_billing_date가true로 설정됩니다. - 취소가 적용되면
subscription.cancelledwebhook이 발생합니다.
구독을 즉시 종료해야 하는 경우(예: 환불 또는 support request에 대한 대응), Customer Portal flow에 의존하지 말고 API를 통해 프로그래밍 방식으로 취소하세요.
프로그래밍 방식으로 취소
언제든지 API를 통해 주문형 구독을 취소할 수 있습니다. 취소를 즉시 처리할지 예약할지 직접 제어합니다. Endpoint: PATCH /subscriptions/{subscription_id}- Cancel immediately
- Cancel at next billing date
구독의
status를 cancelled로 설정하여 즉시 종료합니다. mandate가 취소되고 더 이상 청구를 생성할 수 없습니다.cURL
취소 시 webhook
webhook으로 결과 추적
고객 journey를 추적할 수 있도록 webhook 처리를 구현하세요. Implementing Webhooks을 참조하세요.- subscription.active: mandate가 승인되고 구독이 활성화됨
- subscription.failed: 생성에 실패함(예: mandate 실패)
- subscription.on_hold: 구독이 보류됨(예: 미납 상태)
- subscription.cancelled: 구독이 완전히 취소됨(취소 참조)
- payment.succeeded: 청구가 성공함
- payment.failed: 청구가 실패함
테스트 및 다음 단계
1
Create in test mode
test API key를 사용하여 구독을 생성한 다음 반환된
checkout_url를 열고 mandate를 완료하세요.2
Trigger a charge
소액의
product_price(예: 100)으로 charge endpoint를 호출하고 payment.succeeded를 수신하는지 확인하세요.3
Go live
이벤트와 내부 상태 업데이트를 검증한 후 live API key로 전환하세요.
문제 해결
- 422 Invalid Request:
on_demand.mandate_only가 생성 시 제공되고product_price가 청구 시 제공되는지 확인하세요. - 통화 오류:
product_currency를 재정의하는 경우 해당 통화가 계정과 고객에게 지원되는지 확인하세요. - webhook을 수신하지 못함: webhook URL과 signature secret 구성을 확인하세요.