アップセルとダウンセルでは、保存済みの支払い方法を使って追加商品やプラン変更を顧客に提案できます。これにより支払い情報の再入力なしでワンクリック購入が可能になり、コンバージョン率が劇的に向上します。
Post-Purchase Upsells
チェックアウト直後に補完的な商品をワンクリックで提供します。
Subscription Upgrades
自動按分と即時請求で顧客を上位プランへ移行させます。
Cross-Sells
支払い情報を再入力せずに既存顧客に関連商品を追加します。
概要
アップセルとダウンセルは強力な収益最適化戦略です:- アップセル:より高価値の製品やアップグレード(例:BasicではなくProプラン)を提案
- ダウンセル:顧客が断ったりダウングレードする際により低価格の代替案を提示
- クロスセル:補完的な商品(例:アドオン、関連アイテム)を提案
payment_method_idパラメータを通じてこれらのフローを可能にし、顧客がカード情報を再入力することなく保存済みの支払い方法に課金できます。
主な利点
| 利点 | 効果 |
|---|---|
| ワンクリック購入 | リピーターの決済フォーム入力を省略 |
| コンバージョン向上 | 購入を決断する瞬間の摩擦を軽減 |
| 即時処理 | confirm: true で請求を即時処理 |
| アプリ内体験 | フロー全体を通じて顧客がアプリ内に留まる |
仕組み
前提条件
アップセル・ダウンセルを実装する前に以下を確認してください:- 保存済みの支払い方法を持つ顧客(初回購入後に自動的に保存)
- ダッシュボードで設定したアップセル商品(1回限りの支払い、サブスクリプション、またはアドオン)
payment.succeeded、payment.failed、subscription.plan_changedイベントを処理するよう設定された webhook エンドポイント
顧客の支払い方法の取得
アップセルを提供する前に、顧客の保存済み支払い方法を取得します。- TypeScript
- Python
- Go
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
environment: 'live_mode',
});
async function getPaymentMethods(customerId: string) {
const paymentMethods = await client.customers.retrievePaymentMethods(customerId);
// Returns { items: [...] } — the list of saved payment methods.
// Each item has: payment_method_id, payment_method, payment_method_type, last_used_at,
// recurring_enabled, and card (last4_digits, card_network, card_type, expiry_month, expiry_year)
return paymentMethods;
}
// Example usage
const methods = await getPaymentMethods('cus_123');
console.log('Available payment methods:', methods);
// Use the first available method for upsell
const primaryMethod = methods.items[0]?.payment_method_id;
import os
from dodopayments import DodoPayments
client = DodoPayments(
bearer_token=os.environ.get("DODO_PAYMENTS_API_KEY"),
environment="live_mode",
)
def get_payment_methods(customer_id: str):
payment_methods = client.customers.retrieve_payment_methods(customer_id)
# Returns an object with an `items` list of saved payment methods.
# Each item has: payment_method_id, payment_method, payment_method_type, last_used_at,
# recurring_enabled, and card (last4_digits, card_network, card_type, expiry_month, expiry_year)
return payment_methods
# Example usage
methods = get_payment_methods("cus_123")
print("Available payment methods:", methods)
# Use the first available method for upsell
primary_method = methods.items[0].payment_method_id if methods.items else None
package main
import (
"context"
"fmt"
"os"
"github.com/dodopayments/dodopayments-go"
"github.com/dodopayments/dodopayments-go/option"
)
func getPaymentMethods(customerID string) ([]dodopayments.CustomerGetPaymentMethodsResponseItem, error) {
client := dodopayments.NewClient(
option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
)
resp, err := client.Customers.GetPaymentMethods(
context.TODO(),
customerID,
)
if err != nil {
return nil, err
}
return resp.Items, nil
}
func main() {
methods, err := getPaymentMethods("cus_123")
if err != nil {
panic(err)
}
fmt.Println("Available payment methods:", methods)
// Use the first available method for upsell
if len(methods) > 0 {
primaryMethod := methods[0].PaymentMethodID
fmt.Println("Primary method:", primaryMethod)
}
}
顧客がチェックアウトを完了すると、支払い方法は自動的に保存されます。明示的に保存する必要はありません。
購入後のワンクリックアップセル
購入が正常に完了した直後に追加の商品を提供します。支払い方法はすでに保存されているため、顧客はワンクリックで購入できます。実装
- TypeScript
- Python
- Go
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
environment: 'live_mode',
});
async function createOneClickUpsell(
customerId: string,
paymentMethodId: string,
upsellProductId: string
) {
// Create checkout session with saved payment method
// confirm: true processes the payment immediately
const session = await client.checkoutSessions.create({
product_cart: [
{
product_id: upsellProductId,
quantity: 1
}
],
customer: {
customer_id: customerId
},
payment_method_id: paymentMethodId,
confirm: true, // Required when using payment_method_id
return_url: 'https://yourapp.com/upsell-success',
feature_flags: {
redirect_immediately: true // Skip success page
},
metadata: {
upsell_source: 'post_purchase',
original_order_id: 'order_123'
}
});
return session;
}
// Example: Offer premium add-on after initial purchase
async function handlePostPurchaseUpsell(customerId: string) {
// Get customer's payment methods
const methods = await client.customers.retrievePaymentMethods(customerId);
if (methods.items.length === 0) {
console.log('No saved payment methods available');
return null;
}
// Create the upsell with one-click checkout
const upsell = await createOneClickUpsell(
customerId,
methods.items[0].payment_method_id,
'pdt_premium_addon'
);
console.log('Upsell processed:', upsell.session_id);
return upsell;
}
import os
from dodopayments import DodoPayments
client = DodoPayments(
bearer_token=os.environ.get("DODO_PAYMENTS_API_KEY"),
environment="live_mode",
)
def create_one_click_upsell(
customer_id: str,
payment_method_id: str,
upsell_product_id: str
):
"""Create a one-click upsell using saved payment method."""
# Create checkout session with saved payment method
# confirm=True processes the payment immediately
session = client.checkout_sessions.create(
product_cart=[
{
"product_id": upsell_product_id,
"quantity": 1
}
],
customer={
"customer_id": customer_id
},
payment_method_id=payment_method_id,
confirm=True, # Required when using payment_method_id
return_url="https://yourapp.com/upsell-success",
feature_flags={
"redirect_immediately": True # Skip success page
},
metadata={
"upsell_source": "post_purchase",
"original_order_id": "order_123"
}
)
return session
def handle_post_purchase_upsell(customer_id: str):
"""Offer premium add-on after initial purchase."""
# Get customer's payment methods
methods = client.customers.retrieve_payment_methods(customer_id)
if not methods.items:
print("No saved payment methods available")
return None
# Create the upsell with one-click checkout
upsell = create_one_click_upsell(
customer_id=customer_id,
payment_method_id=methods.items[0].payment_method_id,
upsell_product_id="pdt_premium_addon"
)
print(f"Upsell processed: {upsell.session_id}")
return upsell
package main
import (
"context"
"fmt"
"os"
"github.com/dodopayments/dodopayments-go"
"github.com/dodopayments/dodopayments-go/option"
)
func createOneClickUpsell(
customerID string,
paymentMethodID string,
upsellProductID string,
) (*dodopayments.CheckoutSessionResponse, error) {
client := dodopayments.NewClient(
option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
)
// Create checkout session with saved payment method
// Confirm: true processes the payment immediately
session, err := client.CheckoutSessions.New(context.TODO(), dodopayments.CheckoutSessionNewParams{
CheckoutSessionRequest: dodopayments.CheckoutSessionRequestParam{
ProductCart: dodopayments.F([]dodopayments.ProductItemReqParam{
{
ProductID: dodopayments.F(upsellProductID),
Quantity: dodopayments.F(int64(1)),
},
}),
Customer: dodopayments.F[dodopayments.CustomerRequestUnionParam](
dodopayments.AttachExistingCustomerParam{
CustomerID: dodopayments.F(customerID),
},
),
PaymentMethodID: dodopayments.F(paymentMethodID),
Confirm: dodopayments.F(true), // Required when using payment_method_id
ReturnURL: dodopayments.F("https://yourapp.com/upsell-success"),
FeatureFlags: dodopayments.F(dodopayments.CheckoutSessionFlagsParam{
RedirectImmediately: dodopayments.F(true), // Skip success page
}),
Metadata: dodopayments.F(map[string]string{
"upsell_source": "post_purchase",
"original_order_id": "order_123",
}),
},
})
return session, err
}
func handlePostPurchaseUpsell(customerID string) (*dodopayments.CheckoutSessionResponse, error) {
client := dodopayments.NewClient(
option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
)
// Get customer's payment methods
resp, err := client.Customers.GetPaymentMethods(context.TODO(), customerID)
if err != nil {
return nil, err
}
if len(resp.Items) == 0 {
fmt.Println("No saved payment methods available")
return nil, nil
}
// Create the upsell with one-click checkout
upsell, err := createOneClickUpsell(
customerID,
resp.Items[0].PaymentMethodID,
"pdt_premium_addon",
)
if err != nil {
return nil, err
}
fmt.Printf("Upsell processed: %s\n", upsell.SessionID)
return upsell, nil
}
チェックアウトセッションに
payment_method_id を渡す場合は、confirm: true も設定し、既存の customer_id を指定する必要があります。支払い方法はその顧客に属していなければなりません。POST /payments には confirm フィールドがありません。サブスクリプションのアップグレード
自動的な日割り計算処理により、顧客を上位ティアのサブスクリプションプランへ移行します。確定前のプレビュー
プラン変更によって顧客に請求される金額を正確に示すため、必ず変更をプレビューします。- TypeScript
- Python
- Go
async function previewUpgrade(
subscriptionId: string,
newProductId: string
) {
const preview = await client.subscriptions.previewChangePlan(subscriptionId, {
product_id: newProductId,
quantity: 1,
proration_billing_mode: 'difference_immediately'
});
return {
immediateCharge: preview.immediate_charge?.summary,
newPlan: preview.new_plan,
effectiveAt: preview.immediate_charge?.effective_at
};
}
// Show customer the charge before confirming
const preview = await previewUpgrade('sub_123', 'pdt_pro_plan');
console.log(`Upgrade will charge: ${preview.immediateCharge}`);
def preview_upgrade(subscription_id: str, new_product_id: str):
preview = client.subscriptions.preview_change_plan(
subscription_id=subscription_id,
product_id=new_product_id,
quantity=1,
proration_billing_mode="difference_immediately"
)
return {
"immediate_charge": preview.immediate_charge.summary if preview.immediate_charge else None,
"new_plan": preview.new_plan,
"effective_at": preview.immediate_charge.effective_at if preview.immediate_charge else None,
}
# Show customer the charge before confirming
preview = preview_upgrade("sub_123", "pdt_pro_plan")
print(f"Upgrade will charge: {preview['immediate_charge']}")
func previewUpgrade(subscriptionID string, newProductID string) (map[string]interface{}, error) {
client := dodopayments.NewClient(
option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
)
preview, err := client.Subscriptions.PreviewChangePlan(
context.TODO(),
subscriptionID,
dodopayments.SubscriptionPreviewChangePlanParams{
UpdateSubscriptionPlanReq: dodopayments.UpdateSubscriptionPlanReqParam{
ProductID: dodopayments.F(newProductID),
Quantity: dodopayments.F(int64(1)),
ProrationBillingMode: dodopayments.F(dodopayments.UpdateSubscriptionPlanReqProrationBillingModeDifferenceImmediately),
},
},
)
if err != nil {
return nil, err
}
return map[string]interface{}{
"immediate_charge": preview.ImmediateCharge.Summary,
"new_plan": preview.NewPlan,
"effective_at": preview.ImmediateCharge.EffectiveAt,
}, nil
}
アップグレードの実行
- TypeScript
- Python
- Go
async function upgradeSubscription(
subscriptionId: string,
newProductId: string,
prorationMode: 'prorated_immediately' | 'difference_immediately' | 'full_immediately' | 'do_not_bill' = 'difference_immediately'
) {
// change-plan returns 200 with a ChangePlanResponse body (payment_id, payment_link,
// client_secret, expires_on). All four are null when the change settles off-session.
await client.subscriptions.changePlan(subscriptionId, {
product_id: newProductId,
quantity: 1,
proration_billing_mode: prorationMode
});
// Re-read the subscription to observe the applied state.
return await client.subscriptions.retrieve(subscriptionId);
}
// Upgrade from Basic ($30) to Pro ($80)
// With difference_immediately: charges $50 instantly
const upgrade = await upgradeSubscription('sub_123', 'pdt_pro_plan');
console.log('Upgrade status:', upgrade.status);
def upgrade_subscription(
subscription_id: str,
new_product_id: str,
proration_mode: str = "difference_immediately"
):
# change_plan returns 200 with a ChangePlanResponse body (payment_id, payment_link,
# client_secret, expires_on). All four are None when the change settles off-session.
client.subscriptions.change_plan(
subscription_id=subscription_id,
product_id=new_product_id,
quantity=1,
proration_billing_mode=proration_mode
)
# Re-read the subscription to observe the applied state.
return client.subscriptions.retrieve(subscription_id)
# Upgrade from Basic ($30) to Pro ($80)
# With difference_immediately: charges $50 instantly
upgrade = upgrade_subscription("sub_123", "pdt_pro_plan")
print(f"Upgrade status: {upgrade.status}")
func upgradeSubscription(
subscriptionID string,
newProductID string,
prorationMode dodopayments.UpdateSubscriptionPlanReqProrationBillingMode,
) error {
client := dodopayments.NewClient(
option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
)
// ChangePlan returns a SubscriptionChangePlanResponse alongside the error.
_, err := client.Subscriptions.ChangePlan(
context.TODO(),
subscriptionID,
dodopayments.SubscriptionChangePlanParams{
UpdateSubscriptionPlanReq: dodopayments.UpdateSubscriptionPlanReqParam{
ProductID: dodopayments.F(newProductID),
Quantity: dodopayments.F(int64(1)),
ProrationBillingMode: dodopayments.F(prorationMode),
},
},
)
return err
}
func main() {
// Upgrade from Basic ($30) to Pro ($80)
// With DifferenceImmediately: charges $50 instantly
err := upgradeSubscription(
"sub_123",
"pdt_pro_plan",
dodopayments.UpdateSubscriptionPlanReqProrationBillingModeDifferenceImmediately,
)
if err != nil {
panic(err)
}
fmt.Println("Upgrade succeeded")
}
日割り計算モード
アップグレード時の顧客への請求方法を選択します。difference_immediately
価格差を即時に請求します($30→$80 = $50)。シンプルなアップグレードに適しています。
prorated_immediately
旧プランの未使用期間分をクレジットとして付与し、新プランの1サイクル分を全額請求します。未使用期間をクレジットする場合に適しています。
full_immediately
新プランの料金全額を請求し、残りの期間は考慮しません。請求サイクルをリセットする場合に適しています。
do_not_bill
即時請求なしでプランを変更します。新プランは次回更新時に請求され、元の請求日は維持されます。厚意によるアップグレードや無料移行に適しています。
わかりやすいアップグレードフローには
difference_immediately を使用してください。請求額は単純な価格差になります。現在のプランの未使用期間分を顧客にクレジットし、新プランの1サイクル分を請求する場合は、prorated_immediately を使用してください。クロスセル
既存の顧客が支払い情報を再入力しなくても、関連商品を追加できるようにします。実装
- TypeScript
- Python
- Go
async function createCrossSell(
customerId: string,
paymentMethodId: string,
productId: string,
quantity: number = 1
) {
// Create a one-time payment using saved payment method
// Note: POST /payments is deprecated — prefer checkout sessions for new
// integrations. `payment_method_id` is passed the same way.
const payment = await client.payments.create({
product_cart: [
{
product_id: productId,
quantity: quantity
}
],
customer: { customer_id: customerId },
billing: { country: 'US', city: 'San Francisco', state: 'CA', street: '1 Market St', zipcode: '94105' },
payment_method_id: paymentMethodId,
return_url: 'https://yourapp.com/purchase-complete',
metadata: {
purchase_type: 'cross_sell',
source: 'product_recommendation'
}
});
return payment;
}
// Example: Customer bought a course, offer related ebook
async function offerRelatedProduct(customerId: string, relatedProductId: string) {
const methods = await client.customers.retrievePaymentMethods(customerId);
if (methods.items.length === 0) {
// Fall back to standard checkout
return client.checkoutSessions.create({
product_cart: [{ product_id: relatedProductId, quantity: 1 }],
customer: { customer_id: customerId },
return_url: 'https://yourapp.com/purchase-complete'
});
}
// One-click purchase
return createCrossSell(customerId, methods.items[0].payment_method_id, relatedProductId);
}
def create_cross_sell(
customer_id: str,
payment_method_id: str,
product_id: str,
quantity: int = 1
):
"""Create a one-time payment using saved payment method."""
# Note: POST /payments is deprecated — prefer checkout sessions for new
# integrations. payment_method_id is passed the same way.
payment = client.payments.create(
product_cart=[
{
"product_id": product_id,
"quantity": quantity
}
],
customer={"customer_id": customer_id},
billing={"country": "US", "city": "San Francisco", "state": "CA", "street": "1 Market St", "zipcode": "94105"},
payment_method_id=payment_method_id,
return_url="https://yourapp.com/purchase-complete",
metadata={
"purchase_type": "cross_sell",
"source": "product_recommendation"
}
)
return payment
def offer_related_product(customer_id: str, related_product_id: str):
"""Offer related product with one-click purchase if possible."""
methods = client.customers.retrieve_payment_methods(customer_id)
if not methods.items:
# Fall back to standard checkout
return client.checkout_sessions.create(
product_cart=[{"product_id": related_product_id, "quantity": 1}],
customer={"customer_id": customer_id},
return_url="https://yourapp.com/purchase-complete"
)
# One-click purchase
return create_cross_sell(customer_id, methods.items[0].payment_method_id, related_product_id)
func createCrossSell(
customerID string,
paymentMethodID string,
productID string,
quantity int64,
) (*dodopayments.PaymentNewResponse, error) {
client := dodopayments.NewClient(
option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
)
// Note: POST /payments is deprecated — prefer checkout sessions for new
// integrations. PaymentMethodID is passed the same way.
payment, err := client.Payments.New(context.TODO(), dodopayments.PaymentNewParams{
ProductCart: dodopayments.F([]dodopayments.OneTimeProductCartItemParam{
{
ProductID: dodopayments.F(productID),
Quantity: dodopayments.F(quantity),
},
}),
Customer: dodopayments.F[dodopayments.CustomerRequestUnionParam](
dodopayments.AttachExistingCustomerParam{CustomerID: dodopayments.F(customerID)},
),
Billing: dodopayments.F(dodopayments.BillingAddressParam{
Country: dodopayments.F(dodopayments.CountryCodeUs),
City: dodopayments.F("San Francisco"),
State: dodopayments.F("CA"),
Street: dodopayments.F("1 Market St"),
Zipcode: dodopayments.F("94105"),
}),
PaymentMethodID: dodopayments.F(paymentMethodID),
ReturnURL: dodopayments.F("https://yourapp.com/purchase-complete"),
Metadata: dodopayments.F(map[string]string{
"purchase_type": "cross_sell",
"source": "product_recommendation",
}),
})
return payment, err
}
func offerRelatedProduct(customerID string, relatedProductID string) (interface{}, error) {
client := dodopayments.NewClient(
option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
)
resp, err := client.Customers.GetPaymentMethods(context.TODO(), customerID)
if err != nil {
return nil, err
}
if len(resp.Items) == 0 {
// Fall back to standard checkout
return client.CheckoutSessions.New(context.TODO(), dodopayments.CheckoutSessionNewParams{
CheckoutSessionRequest: dodopayments.CheckoutSessionRequestParam{
ProductCart: dodopayments.F([]dodopayments.ProductItemReqParam{
{ProductID: dodopayments.F(relatedProductID), Quantity: dodopayments.F(int64(1))},
}),
Customer: dodopayments.F[dodopayments.CustomerRequestUnionParam](
dodopayments.AttachExistingCustomerParam{CustomerID: dodopayments.F(customerID)},
),
ReturnURL: dodopayments.F("https://yourapp.com/purchase-complete"),
},
})
}
// One-click purchase
return createCrossSell(customerID, resp.Items[0].PaymentMethodID, relatedProductID, 1)
}
サブスクリプションのダウングレード
顧客が下位ティアのプランへの移行を希望する場合は、自動的にクレジットを適用してスムーズに移行します。ダウングレードの仕組み
- 顧客がダウングレードをリクエスト(Pro → Basic)
- システムが現在のプランの残りの価値を計算
- 今後の更新に使用するクレジットをサブスクリプションに追加
- 顧客を直ちに新しいプランへ移行
プレビューの
immediate_charge.summary.customer_credits は、customer_credits_currency で指定される、顧客のクレジットウォレットの通貨で表示されます。たとえば、顧客が USD サブスクリプションの料金を INR で支払う場合など、サマリーの currency とは異なることがあります。- TypeScript
- Python
- Go
async function downgradeSubscription(
subscriptionId: string,
newProductId: string
) {
// Preview the downgrade first
const preview = await client.subscriptions.previewChangePlan(subscriptionId, {
product_id: newProductId,
quantity: 1,
proration_billing_mode: 'difference_immediately'
});
// customer_credits is positive when credit is added to the balance, negative when credit is used
console.log('Customer credit change:', preview.immediate_charge.summary.customer_credits);
// Execute the downgrade. change-plan returns 200 with a ChangePlanResponse body.
await client.subscriptions.changePlan(subscriptionId, {
product_id: newProductId,
quantity: 1,
proration_billing_mode: 'difference_immediately'
});
// Credits are automatically applied to future renewals
return await client.subscriptions.retrieve(subscriptionId);
}
// Downgrade from Pro ($80) to Basic ($30)
// $50 credit added to subscription, auto-applied on next renewal
const downgrade = await downgradeSubscription('sub_123', 'pdt_basic_plan');
def downgrade_subscription(subscription_id: str, new_product_id: str):
# Preview the downgrade first
preview = client.subscriptions.preview_change_plan(
subscription_id=subscription_id,
product_id=new_product_id,
quantity=1,
proration_billing_mode="difference_immediately"
)
# customer_credits is positive when credit is added to the balance, negative when credit is used
print(f"Customer credit change: {preview.immediate_charge.summary.customer_credits}")
# Execute the downgrade. change_plan returns 200 with a ChangePlanResponse body.
client.subscriptions.change_plan(
subscription_id=subscription_id,
product_id=new_product_id,
quantity=1,
proration_billing_mode="difference_immediately"
)
# Credits are automatically applied to future renewals
return client.subscriptions.retrieve(subscription_id)
# Downgrade from Pro ($80) to Basic ($30)
# $50 credit added to subscription, auto-applied on next renewal
downgrade = downgrade_subscription("sub_123", "pdt_basic_plan")
func downgradeSubscription(subscriptionID string, newProductID string) error {
client := dodopayments.NewClient(
option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
)
// Preview the downgrade first
preview, err := client.Subscriptions.PreviewChangePlan(
context.TODO(),
subscriptionID,
dodopayments.SubscriptionPreviewChangePlanParams{
UpdateSubscriptionPlanReq: dodopayments.UpdateSubscriptionPlanReqParam{
ProductID: dodopayments.F(newProductID),
Quantity: dodopayments.F(int64(1)),
ProrationBillingMode: dodopayments.F(dodopayments.UpdateSubscriptionPlanReqProrationBillingModeDifferenceImmediately),
},
},
)
if err != nil {
return err
}
fmt.Printf("Customer credits to be applied: %v\n", preview.ImmediateCharge.Summary.CustomerCredits)
// Execute the downgrade (returns a SubscriptionChangePlanResponse alongside the error)
_, err = client.Subscriptions.ChangePlan(
context.TODO(),
subscriptionID,
dodopayments.SubscriptionChangePlanParams{
UpdateSubscriptionPlanReq: dodopayments.UpdateSubscriptionPlanReqParam{
ProductID: dodopayments.F(newProductID),
Quantity: dodopayments.F(int64(1)),
ProrationBillingMode: dodopayments.F(dodopayments.UpdateSubscriptionPlanReqProrationBillingModeDifferenceImmediately),
},
},
)
return err
}
difference_immediately を使用したダウングレードによるクレジットはサブスクリプション単位で適用され、今後の更新に自動的に充当されます。これらはクレジットベース課金のエンタイトルメントとは異なります。購入後のアップセルフロー:完全な例
購入が成功した後にアップセルを提供する方法を示す、完全な実装例を紹介します。- TypeScript
- Python
import DodoPayments from 'dodopayments';
import express from 'express';
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
environment: 'live_mode',
});
const app = express();
// Store for tracking upsell eligibility (use your database in production)
const eligibleUpsells = new Map<string, { customerId: string; productId: string }>();
// Webhook handler for initial purchase success
app.post('/webhooks/dodo', express.raw({ type: 'application/json' }), async (req, res) => {
const event = JSON.parse(req.body.toString());
switch (event.type) {
case 'payment.succeeded':
// Check if customer is eligible for upsell
const customerId = event.data.customer.customer_id;
const productId = event.data.product_cart?.[0]?.product_id;
// Define upsell rules (e.g., bought Basic, offer Pro)
const upsellProduct = getUpsellProduct(productId);
if (upsellProduct) {
eligibleUpsells.set(customerId, {
customerId,
productId: upsellProduct
});
}
break;
case 'payment.failed':
console.log('Payment failed:', event.data.payment_id);
// Handle failed upsell payment
break;
}
res.json({ received: true });
});
// API endpoint to check upsell eligibility
app.get('/api/upsell/:customerId', async (req, res) => {
const { customerId } = req.params;
const upsell = eligibleUpsells.get(customerId);
if (!upsell) {
return res.json({ eligible: false });
}
// Get payment methods
const methods = await client.customers.retrievePaymentMethods(customerId);
if (methods.items.length === 0) {
return res.json({ eligible: false, reason: 'no_payment_method' });
}
// Get product details for display
const product = await client.products.retrieve(upsell.productId);
res.json({
eligible: true,
product: {
id: product.product_id,
name: product.name,
price: product.price,
currency: product.price.currency
},
paymentMethodId: methods.items[0].payment_method_id
});
});
// API endpoint to accept upsell
app.post('/api/upsell/:customerId/accept', async (req, res) => {
const { customerId } = req.params;
const upsell = eligibleUpsells.get(customerId);
if (!upsell) {
return res.status(400).json({ error: 'No upsell available' });
}
try {
const methods = await client.customers.retrievePaymentMethods(customerId);
// Create one-click purchase
const session = await client.checkoutSessions.create({
product_cart: [{ product_id: upsell.productId, quantity: 1 }],
customer: { customer_id: customerId },
payment_method_id: methods.items[0].payment_method_id,
confirm: true,
return_url: `${process.env.APP_URL}/upsell-success`,
feature_flags: { redirect_immediately: true },
metadata: { upsell: 'true', source: 'post_purchase' }
});
// Clear the upsell offer
eligibleUpsells.delete(customerId);
res.json({ success: true, sessionId: session.session_id });
} catch (error) {
console.error('Upsell failed:', error);
res.status(500).json({ error: 'Upsell processing failed' });
}
});
// Helper function to determine upsell product
function getUpsellProduct(purchasedProductId: string): string | null {
const upsellMap: Record<string, string> = {
'pdt_basic_plan': 'pdt_pro_plan',
'pdt_starter_course': 'pdt_complete_bundle',
'pdt_single_license': 'pdt_team_license'
};
return upsellMap[purchasedProductId] || null;
}
app.listen(3000);
import os
from flask import Flask, request, jsonify
from dodopayments import DodoPayments
client = DodoPayments(
bearer_token=os.environ.get("DODO_PAYMENTS_API_KEY"),
environment="live_mode",
)
app = Flask(__name__)
# Store for tracking upsell eligibility (use your database in production)
eligible_upsells = {}
@app.route('/webhooks/dodo', methods=['POST'])
def webhook_handler():
event = request.json
if event['type'] == 'payment.succeeded':
# Check if customer is eligible for upsell
customer_id = event['data']['customer']['customer_id']
product_id = (event['data'].get('product_cart') or [{}])[0].get('product_id')
# Define upsell rules
upsell_product = get_upsell_product(product_id)
if upsell_product:
eligible_upsells[customer_id] = {
'customer_id': customer_id,
'product_id': upsell_product
}
elif event['type'] == 'payment.failed':
print(f"Payment failed: {event['data']['payment_id']}")
return jsonify({'received': True})
@app.route('/api/upsell/<customer_id>', methods=['GET'])
def check_upsell(customer_id):
upsell = eligible_upsells.get(customer_id)
if not upsell:
return jsonify({'eligible': False})
# Get payment methods
methods = client.customers.retrieve_payment_methods(customer_id)
if not methods.items:
return jsonify({'eligible': False, 'reason': 'no_payment_method'})
# Get product details for display
product = client.products.retrieve(upsell['product_id'])
return jsonify({
'eligible': True,
'product': {
'id': product.product_id,
'name': product.name,
'price': product.price,
'currency': product.price.currency
},
'payment_method_id': methods.items[0].payment_method_id
})
@app.route('/api/upsell/<customer_id>/accept', methods=['POST'])
def accept_upsell(customer_id):
upsell = eligible_upsells.get(customer_id)
if not upsell:
return jsonify({'error': 'No upsell available'}), 400
try:
methods = client.customers.retrieve_payment_methods(customer_id)
# Create one-click purchase
session = client.checkout_sessions.create(
product_cart=[{'product_id': upsell['product_id'], 'quantity': 1}],
customer={'customer_id': customer_id},
payment_method_id=methods.items[0].payment_method_id,
confirm=True,
return_url=f"{os.environ['APP_URL']}/upsell-success",
feature_flags={'redirect_immediately': True},
metadata={'upsell': 'true', 'source': 'post_purchase'}
)
# Clear the upsell offer
del eligible_upsells[customer_id]
return jsonify({'success': True, 'session_id': session.session_id})
except Exception as error:
print(f"Upsell failed: {error}")
return jsonify({'error': 'Upsell processing failed'}), 500
def get_upsell_product(purchased_product_id: str) -> str:
"""Determine upsell product based on purchased product."""
upsell_map = {
'pdt_basic_plan': 'pdt_pro_plan',
'pdt_starter_course': 'pdt_complete_bundle',
'pdt_single_license': 'pdt_team_license'
}
return upsell_map.get(purchased_product_id)
if __name__ == '__main__':
app.run(port=3000)
ベストプラクティス
- タイミングを戦略的に設定する:顧客が購入意欲を持っている購入成功直後にアップセルを提供します。その他の効果的なタイミングとして、機能の利用におけるマイルストーン達成時、プランの上限に近づいたとき、オンボーディング完了時などがあります。
- 支払い方法を検証する:ワンクリック決済を実行する前に、支払い方法が商品の通貨に対応していること、有効期限が切れていないこと、顧客本人のものであることを確認します。
- 失敗に適切に対処する:ワンクリック決済に失敗した場合は、標準のチェックアウトフローにフォールバックし、明確なメッセージで顧客に通知して、支払い方法の更新を案内します。
- 価値を明確に示す:顧客が現在のプランと比べて何を得られるのかを示し、合計金額ではなく価格差を強調し、ソーシャルプルーフを活用します。
- 顧客の選択を尊重する:常に簡単に辞退できる方法を提供し、辞退後に同じアップセルを繰り返し表示せず、どのアップセルが成約につながったかを追跡してオファーを最適化します。
監視する Webhook
アップセルおよびダウングレードフローでは、次の Webhook イベントを追跡します。| イベント | トリガー | アクション |
|---|---|---|
payment.succeeded | アップセル/クロスセルの支払い完了 | 商品を提供し、アクセス権を更新する |
payment.failed | ワンクリック決済の失敗 | エラーを表示し、再試行またはフォールバックを案内する |
subscription.plan_changed | アップグレード/ダウングレードの完了 | 機能を更新し、確認通知を送信する |
subscription.active | プラン変更後のサブスクリプション再開 | 新しいティアへのアクセス権を付与する |
Webhook Integration Guide
Webhook エンドポイントの設定と検証方法を確認できます。
関連リソース
Subscription Upgrade Guide
プラン変更、日割り計算モード、失敗への対処方法に関する詳細ガイドです。
Checkout Sessions
すべてのオプションを使用してチェックアウトセッションを作成するための完全なリファレンスです。
Customer Payment Methods API
顧客の支払い方法を一覧表示するための API リファレンスです。
Add-ons
追加の収益を得るために、柔軟なアドオンでサブスクリプションを拡張します。