온디맨드 구독을 사용하면 고객의 결제 수단을 한 번 승인한 후, 고정된 일정 대신 필요할 때마다 가변 금액을 청구할 수 있습니다.
이 기능은 귀하의 계정에서 활성화해야 할 수 있습니다. 대시보드에서 보이지 않는 경우 지원팀에 문의하십시오.
이 가이드를 사용하여:
- 온디맨드 구독 생성(선택적 초기 가격으로 위임 승인)
- 사용자 정의 금액으로 후속 요금 트리거
- 웹훅을 사용하여 결과 추적
일반 구독 설정에 대한 내용은 구독 통합 가이드를 참조하십시오.
전제 조건
- Dodo Payments 상인 계정 및 API 키
- 웹훅 비밀이 구성되어 있고 이벤트를 수신할 엔드포인트
- 카탈로그에 구독 제품이 있어야 합니다.
고객이 호스팅된 체크아웃을 통해 위임을 승인하도록 하려면 payment_link: true를 설정하고 return_url을 제공하십시오.
온디맨드 작동 방식
on_demand 객체를 사용하여 결제 수단을 승인하고 선택적으로 초기 요금을 수집하는 구독을 생성합니다.
- 나중에 전용 요금 엔드포인트를 사용하여 해당 구독에 대해 사용자 정의 금액으로 요금을 생성합니다.
- 웹훅(예:
payment.succeeded, payment.failed)을 수신하여 시스템을 업데이트합니다.
온디맨드 구독 생성
엔드포인트: POST /subscriptions
주요 요청 필드(본문):
기존 고객을 연결하거나 고객 세부정보를 제공합니다.
true인 경우, 위임 승인을 위한 호스팅된 체크아웃 링크를 생성하고 선택적 초기 결제를 제공합니다.
호스팅된 체크아웃을 완료한 후 고객을 리디렉션할 위치입니다.
true인 경우, 생성 중에 고객에게 요금을 청구하지 않고 결제 수단을 승인합니다.
초기 청구 금액(가장 작은 통화 단위). 지정된 경우, 이 값은 제품 생성 시 설정된 원래 가격을 덮어씁니다. 생략하면 제품의 저장된 가격이 사용됩니다. 예: $1.00를 청구하려면 100을 전달하십시오.
on_demand.product_currency
초기 청구에 대한 선택적 통화 재정의입니다. 기본값은 제품 통화입니다.
on_demand.product_description
청구 및 항목에 대한 선택적 설명 재정의입니다.
on_demand.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,
environment: 'test_mode', // defaults to 'live_mode'
});
async function main() {
const subscription = await client.subscriptions.create({
billing: { city: 'SF', country: 'US', state: 'CA', street: '1 Market St', zipcode: '94105' },
customer: { customer_id: 'customer_123' },
product_id: 'prod_sub_123',
quantity: 1,
payment_link: true,
return_url: 'https://example.com/billing/success',
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,
},
});
// If payment_link was true, redirect the customer to authorize the mandate
console.log(subscription.payment_link);
}
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"
)
subscription = client.subscriptions.create(
billing={
"city": "SF",
"country": "US",
"state": "CA",
"street": "1 Market St",
"zipcode": "94105",
},
customer={"customer_id": "customer_123"},
product_id="prod_sub_123",
quantity=1,
payment_link=True,
return_url="https://example.com/billing/success",
on_demand={
"mandate_only": True,
# "product_price": 1000,
# "product_currency": "USD",
# "product_description": "Custom initial charge",
# "adaptive_currency_fees_inclusive": False,
},
)
print(subscription.payment_link)
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"),
)
subscription, err := client.Subscriptions.New(context.TODO(), dodopayments.SubscriptionNewParams{
Billing: dodopayments.F(dodopayments.BillingAddressParam{
City: dodopayments.F("SF"),
Country: dodopayments.F(dodopayments.CountryCodeUs),
State: dodopayments.F("CA"),
Street: dodopayments.F("1 Market St"),
Zipcode: dodopayments.F("94105"),
}),
Customer: dodopayments.F[dodopayments.CustomerRequestUnionParam](dodopayments.AttachExistingCustomerParam{
CustomerID: dodopayments.F("customer_123"),
}),
ProductID: dodopayments.F("prod_sub_123"),
Quantity: dodopayments.F(int64(1)),
PaymentLink: dodopayments.F(true),
ReturnURL: dodopayments.F("https://example.com/billing/success"),
OnDemand: dodopayments.F(dodopayments.OnDemandSubscriptionReqParam{
MandateOnly: dodopayments.F(true),
// ProductPrice: dodopayments.F(int64(1000)),
// ProductCurrency: dodopayments.F(dodopayments.CurrencyUsd),
// ProductDescription: dodopayments.F("Custom initial charge"),
// AdaptiveCurrencyFeesInclusive: dodopayments.F(false),
}),
})
if err != nil { panic(err) }
fmt.Println(subscription.PaymentLink)
}
curl -X POST "$DODO_API/subscriptions" \
-H "Authorization: Bearer $DODO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"billing": {"city": "SF", "country": "US", "state": "CA", "street": "1 Market St", "zipcode": "94105"},
"customer": {"customer_id": "customer_123"},
"product_id": "prod_sub_123",
"quantity": 1,
"payment_link": true,
"return_url": "https://example.com/billing/success",
"on_demand": {
"mandate_only": true
}
}'
payment_link: true를 설정하고, 고객을 payment_link로 리디렉션하여 위임 승인을 완료하십시오.
{
"subscription_id": "sub_123",
"payment_link": "https://pay.dodopayments.com/checkout/...",
"customer": { "customer_id": "customer_123", "email": "[email protected]", "name": "Alex Doe" },
"metadata": {},
"recurring_pre_tax_amount": 0,
"addons": []
}
온디맨드 구독 요금 청구
위임이 승인된 후 필요에 따라 요금을 생성합니다.
엔드포인트: 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,
environment: 'test_mode', // defaults to 'live_mode'
});
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'),
environment="test_mode", # defaults to "live_mode"
)
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 및 서명 비밀 구성 확인.