업셀과 다운셀을 통해 고객이 저장한 결제 수단을 사용하여 추가 상품이나 요금제 변경을 제안할 수 있습니다. 이는 결제 수집을 생략한 원클릭 구매를 가능하게 하여 전환율을 극적으로 향상시킵니다.
Post-Purchase Upsells
체크아웃 직후 보완 상품을 원클릭으로 제공하세요.
Subscription Upgrades
자동 비례 배분과 즉각적인 청구로 고객을 상위 요금제로 이동시키세요.
Cross-Sells
기존 고객에게 결제 정보를 다시 입력받지 않고 관련 상품을 추가하세요.
개요
업셀과 다운셀은 강력한 수익 최적화 전략입니다:- 업셀: 더 높은 가치의 제품 또는 업그레이드 제안 (예: 기본 대신 프로 플랜)
- 다운셀: 고객이 거절하거나 다운그레이드할 때 낮은 가격의 대안 제안
- 교차 판매: 보완 제품 제안 (예: 추가 기능, 관련 항목)
payment_method_id 매개변수를 통해 이러한 흐름을 가능하게 하며, 고객이 카드 정보를 다시 입력하지 않아도 저장된 결제 수단을 청구할 수 있게 해줍니다.
주요 이점
| Benefit | Impact |
|---|---|
| One-click purchases | 기존 고객을 위해 결제 양식을 완전히 생략 |
| Higher conversion | 의사결정 순간의 마찰을 줄임 |
| Instant processing | confirm: true으로 청구가 즉시 처리됨 |
| Seamless UX | 흐름 전체에서 고객이 앱을 떠나지 않음 |
작동 방식
필수 조건
업셀과 다운셀을 구현하기 전에 다음을 확인하세요:고객 결제 방법 가져오기
업셀을 제안하기 전에 고객의 저장된 결제 방법을 가져옵니다:- 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.listPaymentMethods(customerId);
// Returns array of saved payment methods
// Each has: payment_method_id, type, card (last4, brand, exp_month, exp_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[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.list_payment_methods(customer_id)
# Returns list of saved payment methods
# Each has: payment_method_id, type, card (last4, brand, exp_month, exp_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[0].payment_method_id if methods else None
복사
package main
import (
"context"
"fmt"
"github.com/dodopayments/dodopayments-go"
"github.com/dodopayments/dodopayments-go/option"
)
func getPaymentMethods(customerID string) ([]dodopayments.PaymentMethod, error) {
client := dodopayments.NewClient(
option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
)
methods, err := client.Customers.ListPaymentMethods(
context.TODO(),
customerID,
)
if err != nil {
return nil, err
}
return methods, 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.listPaymentMethods(customerId);
if (methods.length === 0) {
console.log('No saved payment methods available');
return null;
}
// Create the upsell with one-click checkout
const upsell = await createOneClickUpsell(
customerId,
methods[0].payment_method_id,
'prod_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.list_payment_methods(customer_id)
if not methods:
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[0].payment_method_id,
upsell_product_id="prod_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.CheckoutSession, 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.Create(context.TODO(), dodopayments.CheckoutSessionCreateParams{
ProductCart: dodopayments.F([]dodopayments.CheckoutSessionCreateParamsProductCart{
{
ProductID: dodopayments.F(upsellProductID),
Quantity: dodopayments.F(int64(1)),
},
}),
Customer: dodopayments.F(dodopayments.CheckoutSessionCreateParamsCustomer{
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.CheckoutSessionCreateParamsFeatureFlags{
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.CheckoutSession, error) {
client := dodopayments.NewClient(
option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
)
// Get customer's payment methods
methods, err := client.Customers.ListPaymentMethods(context.TODO(), customerID)
if err != nil {
return nil, err
}
if len(methods) == 0 {
fmt.Println("No saved payment methods available")
return nil, nil
}
// Create the upsell with one-click checkout
upsell, err := createOneClickUpsell(
customerID,
methods[0].PaymentMethodID,
"prod_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를 제공해야 합니다. 결제 수단은 해당 고객의 것이어야 합니다.구독 업그레이드
자동 비율 조정을 통해 고객을 상위 등급 구독 플랜으로 이동시킵니다.커밋 전에 미리보기
항상 고객이 정확히 얼마를 청구받는지 보여주기 위해 플랜 변경을 미리 봅니다:- 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,
effectiveDate: preview.effective_date
};
}
// Show customer the charge before confirming
const preview = await previewUpgrade('sub_123', 'prod_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_date": preview.effective_date
}
# Show customer the charge before confirming
preview = preview_upgrade("sub_123", "prod_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{
ProductID: dodopayments.F(newProductID),
Quantity: dodopayments.F(int64(1)),
ProrationBillingMode: dodopayments.F(dodopayments.ProrationBillingModeDifferenceImmediately),
},
)
if err != nil {
return nil, err
}
return map[string]interface{}{
"immediate_charge": preview.ImmediateCharge.Summary,
"new_plan": preview.NewPlan,
"effective_date": preview.EffectiveDate,
}, nil
}
업그레이드 실행
- TypeScript
- Python
- Go
복사
async function upgradeSubscription(
subscriptionId: string,
newProductId: string,
prorationMode: 'prorated_immediately' | 'difference_immediately' | 'full_immediately' = 'difference_immediately'
) {
const result = await client.subscriptions.changePlan(subscriptionId, {
product_id: newProductId,
quantity: 1,
proration_billing_mode: prorationMode
});
return {
status: result.status,
subscriptionId: result.subscription_id,
paymentId: result.payment_id,
invoiceId: result.invoice_id
};
}
// Upgrade from Basic ($30) to Pro ($80)
// With difference_immediately: charges $50 instantly
const upgrade = await upgradeSubscription('sub_123', 'prod_pro_plan');
console.log('Upgrade status:', upgrade.status);
복사
def upgrade_subscription(
subscription_id: str,
new_product_id: str,
proration_mode: str = "difference_immediately"
):
result = client.subscriptions.change_plan(
subscription_id=subscription_id,
product_id=new_product_id,
quantity=1,
proration_billing_mode=proration_mode
)
return {
"status": result.status,
"subscription_id": result.subscription_id,
"payment_id": result.payment_id,
"invoice_id": result.invoice_id
}
# Upgrade from Basic ($30) to Pro ($80)
# With difference_immediately: charges $50 instantly
upgrade = upgrade_subscription("sub_123", "prod_pro_plan")
print(f"Upgrade status: {upgrade['status']}")
복사
func upgradeSubscription(
subscriptionID string,
newProductID string,
prorationMode dodopayments.ProrationBillingMode,
) (*dodopayments.SubscriptionChangePlanResponse, error) {
client := dodopayments.NewClient(
option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
)
result, err := client.Subscriptions.ChangePlan(
context.TODO(),
subscriptionID,
dodopayments.SubscriptionChangePlanParams{
ProductID: dodopayments.F(newProductID),
Quantity: dodopayments.F(int64(1)),
ProrationBillingMode: dodopayments.F(prorationMode),
},
)
return result, err
}
// Upgrade from Basic ($30) to Pro ($80)
// With DifferenceImmediately: charges $50 instantly
upgrade, err := upgradeSubscription(
"sub_123",
"prod_pro_plan",
dodopayments.ProrationBillingModeDifferenceImmediately,
)
if err != nil {
panic(err)
}
fmt.Printf("Upgrade status: %s\n", upgrade.Status)
비율 조정 모드
업그레이드 시 고객 청구 방법 선택:| Mode | Behavior | Best For |
|---|---|---|
difference_immediately | 가격 차액을 즉시 청구 (30→80 = $50) | 단순 업그레이드 |
prorated_immediately | 청구 주기의 남은 시간에 따라 청구 | 시간 기반 공정 청구 |
full_immediately | 새 요금제의 전체 가격을 청구하며 남은 시간을 무시 | 청구 주기 초기화 |
간단한 업그레이드 흐름에는
difference_immediately를 사용하세요. 현재 요금제에서 남은 시간을 고려하려면 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
const payment = await client.payments.create({
product_cart: [
{
product_id: productId,
quantity: quantity
}
],
customer_id: customerId,
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.listPaymentMethods(customerId);
if (methods.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[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."""
payment = client.payments.create(
product_cart=[
{
"product_id": product_id,
"quantity": quantity
}
],
customer_id=customer_id,
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.list_payment_methods(customer_id)
if not methods:
# 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[0].payment_method_id, related_product_id)
복사
func createCrossSell(
customerID string,
paymentMethodID string,
productID string,
quantity int64,
) (*dodopayments.Payment, error) {
client := dodopayments.NewClient(
option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
)
payment, err := client.Payments.Create(context.TODO(), dodopayments.PaymentCreateParams{
ProductCart: dodopayments.F([]dodopayments.PaymentCreateParamsProductCart{
{
ProductID: dodopayments.F(productID),
Quantity: dodopayments.F(quantity),
},
}),
CustomerID: dodopayments.F(customerID),
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")),
)
methods, err := client.Customers.ListPaymentMethods(context.TODO(), customerID)
if err != nil {
return nil, err
}
if len(methods) == 0 {
// Fall back to standard checkout
return client.CheckoutSessions.Create(context.TODO(), dodopayments.CheckoutSessionCreateParams{
ProductCart: dodopayments.F([]dodopayments.CheckoutSessionCreateParamsProductCart{
{ProductID: dodopayments.F(relatedProductID), Quantity: dodopayments.F(int64(1))},
}),
Customer: dodopayments.F(dodopayments.CheckoutSessionCreateParamsCustomer{CustomerID: dodopayments.F(customerID)}),
ReturnURL: dodopayments.F("https://yourapp.com/purchase-complete"),
})
}
// One-click purchase
return createCrossSell(customerID, methods[0].PaymentMethodID, relatedProductID, 1)
}
구독 다운그레이드
고객이 하위 등급 플랜으로 이동하고자 할 때 자동 크레딧으로 부드럽게 전환을 처리합니다.다운그레이드 작동 방식
- 고객이 다운그레이드 요청 (프로 → 기본)
- 시스템이 현재 플랜의 남은 가치를 계산
- 향후 갱신을 위해 구독에 크레딧 추가
- 고객이 즉시 새로운 플랜으로 이동
- 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'
});
console.log('Credit to be applied:', preview.credit_amount);
// Execute the downgrade
const result = await client.subscriptions.changePlan(subscriptionId, {
product_id: newProductId,
quantity: 1,
proration_billing_mode: 'difference_immediately'
});
// Credits are automatically applied to future renewals
return result;
}
// Downgrade from Pro ($80) to Basic ($30)
// $50 credit added to subscription, auto-applied on next renewal
const downgrade = await downgradeSubscription('sub_123', 'prod_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"
)
print(f"Credit to be applied: {preview.credit_amount}")
# Execute the downgrade
result = 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 result
# Downgrade from Pro ($80) to Basic ($30)
# $50 credit added to subscription, auto-applied on next renewal
downgrade = downgrade_subscription("sub_123", "prod_basic_plan")
복사
func downgradeSubscription(subscriptionID string, newProductID string) (*dodopayments.SubscriptionChangePlanResponse, 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{
ProductID: dodopayments.F(newProductID),
Quantity: dodopayments.F(int64(1)),
ProrationBillingMode: dodopayments.F(dodopayments.ProrationBillingModeDifferenceImmediately),
},
)
if err != nil {
return nil, err
}
fmt.Printf("Credit to be applied: %v\n", preview.CreditAmount)
// Execute the downgrade
result, err := client.Subscriptions.ChangePlan(
context.TODO(),
subscriptionID,
dodopayments.SubscriptionChangePlanParams{
ProductID: dodopayments.F(newProductID),
Quantity: dodopayments.F(int64(1)),
ProrationBillingMode: dodopayments.F(dodopayments.ProrationBillingModeDifferenceImmediately),
},
)
return result, err
}
difference_immediately을(를) 사용한 다운그레이드로 발생한 크레딧은 구독 범위이며 향후 갱신에 자동으로 적용됩니다. 이는 Credit-Based Billing 권한과는 별개입니다.전체 예제: 구매 후 업셀 흐름
성공적인 구매 후 업셀을 제안하는 완전한 구현입니다:- 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_id;
const productId = event.data.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.listPaymentMethods(customerId);
if (methods.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.currency
},
paymentMethodId: methods[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.listPaymentMethods(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[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> = {
'prod_basic_plan': 'prod_pro_plan',
'prod_starter_course': 'prod_complete_bundle',
'prod_single_license': 'prod_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_id']
product_id = event['data']['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.list_payment_methods(customer_id)
if not methods:
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.currency
},
'payment_method_id': methods[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.list_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[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 = {
'prod_basic_plan': 'prod_pro_plan',
'prod_starter_course': 'prod_complete_bundle',
'prod_single_license': 'prod_team_license'
}
return upsell_map.get(purchased_product_id)
if __name__ == '__main__':
app.run(port=3000)
모범 사례
Time Your Upsells Strategically
Time Your Upsells Strategically
성공적인 구매 직후, 고객이 구매 마인드에 있을 때 업셀을 제안하는 것이 가장 좋습니다. 다른 효과적인 시점:
- 기능 사용 이정표 달성 후
- 요금제 한도 접근 시
- 온보딩 완료 시
Validate Payment Method Eligibility
Validate Payment Method Eligibility
원클릭 청구를 시도하기 전에 결제 수단을 확인하세요:
- 해당 상품의 통화와 호환되는가
- 만료되지 않았는가
- 고객의 것인가
Handle Failures Gracefully
Handle Failures Gracefully
원클릭 청구에 실패하면:
- 표준 체크아웃 흐름으로 되돌아가기
- 명확한 메시지로 고객에게 알리기
- 결제 수단 업데이트 제안하기
- 실패한 청구를 반복 시도하지 않기
Provide Clear Value Proposition
Provide Clear Value Proposition
업셀은 고객이 가치를 이해할 때 더 잘 전환됩니다:
- 현재 요금제 대비 무엇을 얻는지 보여주기
- 총 가격이 아닌 가격 차를 강조하기
- 사회적 증거 활용 (예: “가장 인기 있는 업그레이드”)
Respect Customer Choice
Respect Customer Choice
- 항상 거절할 수 있는 쉬운 방법 제공
- 거절한 후 동일한 업셀을 반복해서 보여주지 않기
- 어떤 업셀이 전환되는지 추적하고 분석하여 제안 최적화하기
모니터링을 위한 웹훅
업셀 및 다운그레이드 흐름을 위해 이러한 웹훅 이벤트를 추적합니다:| Event | Trigger | Action |
|---|---|---|
payment.succeeded | 업셀/교차 판매 결제 완료 | 상품 제공, 액세스 업데이트 |
payment.failed | 원클릭 청구 실패 | 오류 표시, 재시도 또는 대체 제안 |
subscription.plan_changed | 업그레이드/다운그레이드 완료 | 기능 업데이트, 확인 메시지 발송 |
| INLINE_CODE_PLACEHOLDER_c4613b9706b26e9b_End | 요금제 변경 후 구독 재활성화 | 새 티어 액세스 부여 |
Webhook Integration Guide
웹훅 엔드포인트를 설정하고 확인하는 방법을 알아보세요.