Upsells và downsells giúp bạn đề xuất thêm sản phẩm hoặc thay đổi gói cho khách hàng thông qua các phương thức thanh toán đã lưu của họ. Điều này cho phép mua hàng chỉ bằng một cú nhấp chuột mà không cần thu thập thanh toán, cải thiện đáng kể tỷ lệ chuyển đổi.
Post-Purchase Upsells Cung cấp các sản phẩm bổ sung ngay sau khi thanh toán với tính năng mua hàng chỉ bằng một cú nhấp chuột.
Subscription Upgrades Chuyển khách hàng sang các cấp cao hơn với việc tự động tính tỷ lệ và thanh toán ngay lập tức.
Cross-Sells Thêm các sản phẩm liên quan cho khách hàng hiện có mà không cần nhập lại thông tin thanh toán.
Tổng quan
Upsells và downsells là những chiến lược tối ưu hóa doanh thu mạnh mẽ:
Upsells : Đề xuất sản phẩm giá trị cao hơn hoặc nâng cấp (ví dụ: gói Pro thay vì Basic)
Downsells : Đưa ra lựa chọn giá thấp hơn khi khách hàng từ chối hoặc hạ cấp
Cross-sells : Gợi ý các sản phẩm bổ sung (ví dụ: add-on, mặt hàng liên quan)
Dodo Payments hỗ trợ các luồng này thông qua tham số payment_method_id, cho phép bạn tính phí lên phương thức thanh toán đã lưu của khách hàng mà không yêu cầu họ nhập lại chi tiết thẻ.
Lợi ích chính
Lợi ích Tác động Mua hàng chỉ với một cú nhấp chuột Bỏ qua hoàn toàn biểu mẫu thanh toán cho khách hàng quay lại Tỷ lệ chuyển đổi cao hơn Giảm độ cản trở ngay thời điểm đưa ra quyết định Xử lý ngay lập tức Các khoản phí được xử lý ngay với confirm: true Trải nghiệm liền mạch Khách hàng vẫn ở trong ứng dụng của bạn trong suốt luồng
Cách hoạt động
Yêu cầu trước
Trước khi triển khai upsells và downsells, hãy đảm bảo bạn có:
Customer with Saved Payment Method
Khách hàng phải đã hoàn tất ít nhất một giao dịch mua. Các phương thức thanh toán được tự động lưu khi khách hàng hoàn tất thanh toán.
Products Configured
Tạo các sản phẩm upsell của bạn trong bảng điều khiển Dodo Payments. Chúng có thể là giao dịch một lần, đăng ký định kỳ hoặc add-on.
Webhook Endpoint
Thiết lập webhook để xử lý các sự kiện payment.succeeded, payment.failed và subscription.plan_changed.
Lấy phương thức thanh toán của khách hàng
Trước khi đề xuất upsell, hãy lấy các phương thức thanh toán đã lưu của khách hàng:
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 )
}
}
Các phương thức thanh toán được tự động lưu khi khách hàng hoàn tất thanh toán. Bạn không cần lưu chúng một cách rõ ràng.
Upsells sau khi mua với một cú nhấp chuột
Đề xuất thêm sản phẩm ngay sau khi giao dịch mua thành công. Khách hàng có thể chấp nhận chỉ với một cú nhấp chuột vì phương thức thanh toán đã được lưu trước đó.
Triển khai
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
}
Khi sử dụng payment_method_id, bạn phải đặt confirm: true và cung cấp một customer_id hiện có. Phương thức thanh toán phải thuộc về khách hàng đó.
Nâng cấp đăng ký
Chuyển khách hàng sang các gói đăng ký cao hơn với việc xử lý tỷ lệ tự động.
Xem trước trước khi xác nhận
Luôn xem trước sự thay đổi gói để cho khách hàng thấy chính xác số tiền họ sẽ bị tính phí:
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
}
Thực hiện nâng cấp
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 )
Chế độ tính tỷ lệ
Chọn cách khách hàng được tính phí khi nâng cấp:
Chế độ Hành vi Phù hợp cho difference_immediatelyTính ngay phần chênh lệch giá (30 → 30→ 30 → 80 = $50) Các luồng nâng cấp đơn giản prorated_immediatelyTính dựa trên thời gian còn lại trong chu kỳ thanh toán Thanh toán công bằng theo thời gian full_immediatelyTính toàn bộ giá của gói mới, bỏ qua thời gian còn lại Khởi động lại chu kỳ thanh toán
Sử dụng difference_immediately cho các luồng nâng cấp đơn giản. Dùng prorated_immediately khi bạn muốn tính đến thời gian chưa sử dụng trong gói hiện tại.
Bán chéo
Thêm các sản phẩm bổ sung cho khách hàng hiện có mà không yêu cầu họ nhập lại thông tin thanh toán.
Triển khai
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 )
}
Hạ cấp đăng ký
Khi khách hàng muốn chuyển sang gói thấp hơn, hãy xử lý chuyển đổi một cách nhẹ nhàng với các khoản tín dụng tự động.
Cách hạ cấp hoạt động
Khách hàng yêu cầu hạ cấp (Pro → Basic)
Hệ thống tính toán giá trị còn lại của gói hiện tại
Tín dụng được thêm vào đăng ký cho các lần gia hạn sau
Khách hàng chuyển sang gói mới ngay lập tức
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
}
Các khoản tín dụng từ việc hạ cấp sử dụng difference_immediately là theo phạm vi đăng ký và tự động áp dụng cho các lần gia hạn sau. Chúng khác với Customer Credits .
Ví dụ hoàn chỉnh: Luồng upsell sau khi mua
Đây là một triển khai hoàn chỉnh cho thấy cách đề xuất upsell sau khi mua thành công:
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 )
Thực hành tốt nhất
Time Your Upsells Strategically
Thời điểm tốt nhất để đề xuất upsell là ngay sau khi mua thành công, khi khách hàng đang trong tâm thế sẵn sàng mua. Các thời điểm hiệu quả khác:
Sau các mốc sử dụng tính năng
Khi gần đạt giới hạn gói
Khi hoàn tất hành trình onboarding
Validate Payment Method Eligibility
Trước khi thử tính phí chỉ với một cú nhấp chuột, hãy xác minh phương thức thanh toán:
Có tương thích với tiền tệ của sản phẩm
Chưa hết hạn
Thuộc về khách hàng
API sẽ tự động xác thực những điều này, nhưng kiểm tra trước giúp cải thiện trải nghiệm người dùng.
Handle Failures Gracefully
Khi các giao dịch tính phí một cú nhấp chuột thất bại:
Trở về luồng thanh toán tiêu chuẩn
Thông báo cho khách hàng với thông điệp rõ ràng
Đề nghị cập nhật phương thức thanh toán
Không cố gắng tính phí thất bại lặp lại
Provide Clear Value Proposition
Upsells mang lại tỷ lệ chuyển đổi tốt hơn khi khách hàng hiểu rõ giá trị:
Hiển thị những gì họ nhận được so với gói hiện tại
Làm nổi bật sự khác biệt giá, không phải tổng chi phí
Sử dụng chứng thực xã hội (ví dụ: “Nâng cấp phổ biến nhất”)
Luôn cung cấp cách từ chối dễ dàng
Không hiển thị cùng một upsell nhiều lần sau khi bị từ chối
Theo dõi và phân tích upsell nào chuyển đổi để tối ưu hóa đề nghị
Webhook cần theo dõi
Theo dõi các sự kiện webhook này cho luồng upsell và hạ cấp:
Sự kiện Kích hoạt Hành động payment.succeededThanh toán upsell/bán chéo hoàn tất Giao sản phẩm, cập nhật quyền truy cập payment.failedTính phí một cú nhấp chuột thất bại Hiển thị lỗi, đề nghị thử lại hoặc dự phòng subscription.plan_changedNâng cấp/hạ cấp hoàn tất Cập nhật tính năng, gửi xác nhận subscription.activeĐăng ký được kích hoạt lại sau khi thay đổi gói Cấp quyền truy cập cấp mới
Webhook Integration Guide Tìm hiểu cách thiết lập và xác minh các điểm cuối webhook.
Tài nguyên liên quan