온디맨드 구독을 사용하면 고객의 결제 수단을 한 번 승인한 후 필요할 때마다 고정된 일정이 아닌 변동 금액을 청구할 수 있습니다. 이 기능은 모든 계정에서 사용할 수 있으며, 승인이 필요하지 않습니다.
이 가이드를 사용하여:
- 온디맨드 구독 생성하기 (선택적 초기 가격으로 위임 승인하기)
- 사용자 정의 금액으로 후속 청구 트리거하기
- 웹훅을 사용하여 결과 추적하기
일반 구독 설정에 대한 내용은 구독 통합 가이드를 참조하세요.
전제 조건
- Dodo Payments 상인 계정 및 API 키
- 이벤트를 수신할 웹훅 비밀 및 엔드포인트 구성
- 카탈로그에 구독 제품이 있어야 합니다.
고객이 호스팅된 체크아웃을 통해 위임을 승인하도록 하려면 payment_link: true를 설정하고 return_url를 제공하세요.
온디맨드 작동 방식
- 결제 수단을 승인하고 선택적으로 초기 요금을 수집하기 위해
on_demand 객체로 구독을 생성합니다.
- 나중에 전용 청구 엔드포인트를 사용하여 해당 구독에 대해 사용자 정의 금액으로 청구를 생성합니다.
- 웹훅(예:
payment.succeeded, payment.failed)를 수신하여 시스템을 업데이트합니다.
온디맨드 구독 생성하기
엔드포인트: POST /checkouts
주요 요청 필드 (본문):
체크아웃 세션 생성하기에서 확인하세요.
온디맨드 구독 생성하기
Node.js SDK
Python SDK
Go SDK
cURL
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
environment: 'test_mode', // defaults to 'live_mode'
});
async function main() {
const subscription = await client.checkoutSessions.create({
product_cart: [{ product_id: 'pdt_123', quantity: 1 }],
billing_address: { city: 'SF', country: 'US', state: 'CA', street: '1 Market St', zipcode: '94105' },
customer: { customer_id: 'cus_123' },
return_url: 'https://example.com/billing/success',
subscription_data: {
on_demand: {
mandate_only: true // set false to collect an initial charge
// product_price: 1000, // optional: charge $10.00 now if mandate_only is false
// product_currency: 'USD',
// product_description: 'Custom initial charge',
// adaptive_currency_fees_inclusive: false,
}
}
});
console.log(subscription.checkout_url);
}
main().catch(console.error);
import os
from dodopayments import DodoPayments
client = DodoPayments(
bearer_token=os.environ.get('DODO_PAYMENTS_API_KEY'),
environment="test_mode", # defaults to "live_mode"
)
checkout = client.checkout_sessions.create(
product_cart=[
{"product_id": "pdt_123", "quantity": 1}
],
billing_address={
"city": "SF",
"country": "US",
"state": "CA",
"street": "1 Market St",
"zipcode": "94105",
},
customer={
"customer_id": "cus_123",
},
return_url="https://example.com/billing/success",
subscription_data={
"on_demand":{
"mandate_only": True, # set False to collect an initial charge
# "product_price": 1000, # optional: charge $10.00 now if mandate_only is false
# "product_currency": "USD",
# "product_description": "Custom initial charge",
# "adaptive_currency_fees_inclusive": False,
}
},
)
print(checkout.checkout_url)
package main
import (
"fmt"
"os"
dodo "github.com/dodopayments/dodopayments-go"
)
func main() {
client := dodo.NewClient(dodo.Config{
BearerToken: os.Getenv("DODO_PAYMENTS_API_KEY"),
Environment: dodo.TestMode, // defaults to LiveMode
})
checkout, err := client.CheckoutSessions.Create(dodo.CheckoutSessionCreateParams{
ProductCart: []dodo.ProductCartItem{
{
ProductID: "pdt_123",
Quantity: 1,
},
},
BillingAddress: &dodo.BillingAddress{
City: "SF",
Country: "US",
State: "CA",
Street: "1 Market St",
Zipcode: "94105",
},
Customer: &dodo.CustomerRef{
CustomerID: "cus_123",
},
ReturnURL: "https://example.com/billing/success",
SubscriptionData: &dodo.SubscriptionData{
OnDemand: &dodo.OnDemandSubscription{
MandateOnly: true, // set false to collect an initial charge
// ProductPrice: 1000, // optional: charge $10.00 now if mandate_only is false
// ProductCurrency: "USD",
// ProductDescription: "Custom initial charge",
// AdaptiveCurrencyFeesInclusive: false,
},
},
})
if err != nil {
panic(err)
}
fmt.Println(checkout.CheckoutURL)
}
curl -X POST "$DODO_API/checkouts" \
-H "Authorization: Bearer $DODO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"product_cart": [
{
"product_id": "pdt_123",
"quantity": 1
}
],
"customer": {
"customer_id": "cus_123"
},
"billing_address": {
"street": "1 Market St",
"city": "SF",
"state": "CA",
"country": "US",
"zipcode": "94105"
},
"subscription_data": {
"on_demand": {
"mandate_only": true
}
},
"return_url": "https://example.com/billing/success"
}'
{
"session_id": "cks_123",
"checkout_url": "https://test.checkout.dodopayments.com/session/cks123"
}
온디맨드 구독 청구하기
위임이 승인된 후 필요에 따라 청구를 생성합니다.
엔드포인트: POST /subscriptions/{subscription_id}/charge
주요 요청 필드 (본문):
청구할 금액 (가장 작은 통화 단위). 예: $25.00를 청구하려면 2500을 전달하세요.
adaptive_currency_fees_inclusive
true인 경우 product_price에 적응형 통화 수수료를 포함합니다. false인 경우 수수료가 추가됩니다.
결제에 대한 추가 메타데이터. 생략할 경우 구독 메타데이터가 사용됩니다.
Node.js SDK
Python SDK
Go SDK
cURL
import DodoPayments from 'dodopayments';
const client = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY });
async function chargeNow(subscriptionId) {
const res = await client.subscriptions.charge(subscriptionId, { product_price: 2500 });
console.log(res.payment_id);
}
chargeNow('sub_123').catch(console.error);
import os
from dodopayments import DodoPayments
client = DodoPayments(bearer_token=os.environ.get('DODO_PAYMENTS_API_KEY'))
response = client.subscriptions.charge(
subscription_id="sub_123",
product_price=2500,
)
print(response.payment_id)
package main
import (
"context"
"fmt"
"github.com/dodopayments/dodopayments-go"
"github.com/dodopayments/dodopayments-go/option"
)
func main() {
client := dodopayments.NewClient(option.WithBearerToken("YOUR_API_KEY"))
res, err := client.Subscriptions.Charge(context.TODO(), "sub_123", dodopayments.SubscriptionChargeParams{
ProductPrice: dodopayments.F(int64(2500)),
})
if err != nil { panic(err) }
fmt.Println(res.PaymentID)
}
curl -X POST "$DODO_API/subscriptions/sub_123/charge" \
-H "Authorization: Bearer $DODO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"product_price": 2500,
"product_description": "Extra usage for March"
}'
{ "payment_id": "pay_abc123" }
온디맨드가 아닌 구독에 대해 청구하면 실패할 수 있습니다. 청구하기 전에 구독에 on_demand: true가 포함되어 있는지 확인하세요.
결제 재시도
우리의 사기 탐지 시스템은 공격적인 재시도 패턴을 차단할 수 있으며 (잠재적인 카드 테스트로 표시될 수 있음) 안전한 재시도 정책을 따르세요.
버스트 재시도 패턴은 우리의 위험 시스템 및 프로세서에 의해 사기 또는 의심스러운 카드 테스트로 표시될 수 있습니다. 클러스터 재시도를 피하고 아래의 백오프 일정 및 시간 정렬 지침을 따르세요.
안전한 재시도 정책을 위한 원칙
- 백오프 메커니즘: 재시도 간에 지수 백오프를 사용하세요.
- 재시도 한도: 총 재시도를 제한하세요 (최대 3-4회 시도).
- 지능형 필터링: 재시도 가능한 실패(예: 네트워크/발급자 오류, 자금 부족)에서만 재시도하세요; 하드 거부는 재시도하지 마세요.
- 카드 테스트 방지:
DO_NOT_HONOR, STOLEN_CARD, LOST_CARD, PICKUP_CARD, FRAUDULENT, AUTHENTICATION_FAILURE와 같은 실패는 재시도하지 마세요.
- 메타데이터 다양화 (선택적): 자체 재시도 시스템을 유지하는 경우 메타데이터를 통해 재시도를 구분하세요 (예:
retry_attempt).
권장 재시도 일정 (구독)
- 1차 시도: 청구를 생성할 때 즉시
- 2차 시도: 3일 후
- 3차 시도: 7일 후 (총 10일)
- 4차 시도 (최종): 7일 후 (총 17일)
최종 단계: 여전히 미납인 경우, 정책에 따라 구독을 미납으로 표시하거나 취소하세요. 고객에게 결제 수단을 업데이트할 수 있는 기간 동안 알림을 보내세요.
버스트 재시도를 피하고 승인 시간에 맞추기
- 재시도를 원래 승인 타임스탬프에 고정하여 포트폴리오 전반에 걸쳐 “버스트” 동작을 피하세요.
- 예: 고객이 오늘 오후 1:10에 체험판 또는 위임을 시작하면, 후속 재시도를 백오프에 따라 다음 날 오후 1:10에 예약하세요 (예: +3일 → 오후 1:10, +7일 → 오후 1:10).
- 또는 마지막 성공적인 결제 시간을 저장하는 경우
T, 다음 시도를 T + X days에 예약하여 시간대 정렬을 유지하세요.
시간대 및 DST: 일정을 위해 일관된 시간 표준을 사용하고 표시를 위해서만 변환하여 간격을 유지하세요.
재시도하지 말아야 할 거부 코드
STOLEN_CARD
DO_NOT_HONOR
FRAUDULENT
PICKUP_CARD
AUTHENTICATION_FAILURE
LOST_CARD
거부 사유의 포괄적인 목록과 사용자가 수정할 수 있는지 여부는
거래 실패 문서를 참조하세요.
소프트/임시 문제에서만 재시도하세요 (예: insufficient_funds, issuer_unavailable, processing_error, 네트워크 타임아웃). 동일한 거부가 반복되면 추가 재시도를 중단하세요.
구현 지침 (코드 없음)
- 정확한 타임스탬프를 지속하는 스케줄러/큐를 사용하세요; 다음 시도를 정확한 시간대 오프셋에서 계산하세요 (예:
T + 3 days에서 같은 HH:MM).
- 마지막 성공적인 결제 타임스탬프
T를 유지하고 참조하여 다음 시도를 계산하세요; 여러 구독을 동시에 묶지 마세요.
- 마지막 거부 사유를 항상 평가하세요; 위의 스킵 목록에서 하드 거부에 대한 재시도를 중단하세요.
- 고객 및 계정당 동시 재시도를 제한하여 우발적인 급증을 방지하세요.
- 사전적으로 소통하세요: 다음 예정된 시도 전에 고객에게 결제 수단을 업데이트하라는 이메일/SMS를 보내세요.
- 메타데이터는 관찰 가능성을 위해서만 사용하세요 (예:
retry_attempt); 중요하지 않은 필드를 회전하여 사기/위험 시스템을 “회피”하려고 하지 마세요.
웹훅으로 결과 추적하기
웹훅 처리를 구현하여 고객 여정을 추적하세요. 웹훅 구현하기를 참조하세요.
- subscription.active: 위임이 승인되고 구독이 활성화됨
- subscription.failed: 생성 실패 (예: 위임 실패)
- subscription.on_hold: 구독이 보류됨 (예: 미납 상태)
- payment.succeeded: 청구 성공
- payment.failed: 청구 실패
온디맨드 흐름의 경우, 사용 기반 청구를 조정하기 위해 payment.succeeded 및 payment.failed에 집중하세요.
테스트 및 다음 단계
테스트 모드에서 생성하기
테스트 API 키를 사용하여 payment_link: true로 구독을 생성한 후, 링크를 열고 위임을 완료하세요.
청구 트리거하기
작은 product_price (예: 100)로 청구 엔드포인트를 호출하고 payment.succeeded를 수신하는지 확인하세요.
라이브로 전환하기
이벤트 및 내부 상태 업데이트를 검증한 후 라이브 API 키로 전환하세요.
문제 해결
- 422 잘못된 요청: 생성 시
on_demand.mandate_only가 제공되었는지 확인하고, 청구를 위해 product_price가 제공되었는지 확인하세요.
- 통화 오류:
product_currency를 재정의하는 경우, 해당 통화가 귀하의 계정 및 고객에게 지원되는지 확인하세요.
- 웹훅을 수신하지 못함: 웹훅 URL 및 서명 비밀 구성 확인.