Gli upsell e downsell ti permettono di offrire prodotti aggiuntivi o cambi di piano ai clienti utilizzando i loro metodi di pagamento salvati. Ciò consente acquisti con un clic che saltano la raccolta dei pagamenti, migliorando notevolmente i tassi di conversione.
Upsell post-acquisto Offri prodotti complementari immediatamente dopo il checkout con acquisti con un clic.
Aggiornamenti di abbonamento Porta i clienti a livelli superiori con addebiti automatici e fatturazione istantanea.
Cross-sell Aggiungi prodotti correlati ai clienti esistenti senza reinserire i dettagli di pagamento.
Panoramica
Gli upsell e downsell sono potenti strategie di ottimizzazione dei ricavi:
Upsell : Offri un prodotto o un aggiornamento di valore superiore (es. piano Pro invece del piano Basic)
Downsell : Offri un’alternativa a prezzo inferiore quando un cliente rifiuta o downgrade
Cross-sell : Suggerisci prodotti complementari (es. addon, articoli correlati)
Dodo Payments abilita questi flussi attraverso il payment_method_id parametro, che ti consente di addebitare il metodo di pagamento salvato di un cliente senza richiedere la reinserzione dei dettagli della carta.
Vantaggi chiave
Vantaggio Impatto Acquisti con un clic Salta completamente il modulo di pagamento per i clienti di ritorno Maggiore conversione Riduci le frizioni nel momento decisionale Elaborazione istantanea Gli addebiti vengono elaborati immediatamente con confirm: true Esperienza utente fluida I clienti rimangono nella tua app durante tutto il flusso
Come funziona
Requisiti
Prima di implementare upsell e downsell, assicurati di avere:
Cliente con metodo di pagamento salvato
I clienti devono aver completato almeno un acquisto. I metodi di pagamento vengono salvati automaticamente quando i clienti completano il checkout.
Prodotti configurati
Crea i tuoi prodotti upsell nel dashboard di Dodo Payments. Possono essere pagamenti una tantum, abbonamenti o addon.
Webhook Endpoint
Configura i webhook per gestire payment.succeeded, payment.failed, e subscription.plan_changed eventi.
Ottieni i metodi di pagamento dei clienti
Prima di offrire un upsell, recupera i metodi di pagamento salvati del cliente:
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 )
}
}
I metodi di pagamento vengono salvati automaticamente quando i clienti completano il checkout. Non è necessario salvarli esplicitamente.
Upsell post-acquisto con un clic
Offri prodotti aggiuntivi immediatamente dopo un acquisto riuscito. Il cliente può accettare con un solo clic poiché il suo metodo di pagamento è già salvato.
Implementazione
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
}
Quando utilizzi payment_method_id, devi impostare confirm: true e fornire un customer_id esistente. Il metodo di pagamento deve appartenere a quel cliente.
Aggiornamenti di Abbonamento
Sposta i clienti a piani di abbonamento di livello superiore con gestione automatica della proporzione.
Anteprima prima di impegnarsi
Anteprima sempre le modifiche al piano per mostrare ai clienti esattamente cosa verrà addebitato:
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
}
Esegui l’aggiornamento
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 )
Modalità di proporzione
Scegli come vengono addebitati i clienti quando aggiornano:
Modalità Comportamento Migliore per difference_immediatelyAddebita la differenza di prezzo istantaneamente (30 → 30→ 30 → 80 = $50) Aggiornamenti semplici prorated_immediatelyAddebita in base al tempo rimanente nel ciclo di fatturazione Fatturazione equa basata sul tempo full_immediatelyAddebita l’intero prezzo del nuovo piano, ignora il tempo rimanente Il ciclo di fatturazione si resetta
Utilizza difference_immediately per flussi di aggiornamento diretti. Usa prorated_immediately quando desideri tenere conto del tempo non utilizzato nel piano attuale.
Cross-Sell
Aggiungi prodotti complementari per i clienti esistenti senza richiederne il reinserimento dei dettagli di pagamento.
Implementazione
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 )
}
Downgrade dell’abbonamento
Quando i clienti vogliono passare a un piano di livello inferiore, gestisci la transizione in modo fluido con crediti automatici.
Come funzionano i downgrade
Il cliente richiede il downgrade (Pro → Basic)
Il sistema calcola il valore rimanente del piano attuale
Il credito viene aggiunto all’abbonamento per i rinnovi futuri
Il cliente passa immediatamente al nuovo piano
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
}
I crediti dai downgrade utilizzando difference_immediately sono limitati all’abbonamento e applicati automaticamente ai rinnovi futuri. Sono distinti dai Crediti Cliente .
Esempio completo: Flusso di upsell post-acquisto
Ecco un’implementazione completa che mostra come offrire un upsell dopo un acquisto riuscito:
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 )
Migliori Pratiche
Tempistica strategica degli upsell
Il momento migliore per offrire un upsell è immediatamente dopo un acquisto riuscito quando i clienti sono nella mentalità di acquisto. Altri momenti efficaci:
Dopo le pietre miliari nell’uso delle funzionalità
Quando ci si avvicina ai limiti del piano
Durante il completamento dell’onboarding
Valida l'idoneità del metodo di pagamento
Prima di tentare un addebito con un clic, verifica il metodo di pagamento:
È compatibile con la valuta del prodotto
Non è scaduto
Appartiene al cliente
L’API validerà questi aspetti, ma controllare proattivamente migliora l’UX.
Gestisci i fallimenti con grazia
Quando i tentativi di addebito con un clic falliscono:
Passa al flusso di checkout standard
Notifica il cliente con messaggi chiari
Offri di aggiornare il metodo di pagamento
Non tentare ripetutamente addebiti falliti
Fornisci una chiara proposta di valore
Gli upsell convertono meglio quando i clienti comprendono il valore:
Mostra cosa stanno ricevendo rispetto al piano attuale
Evidenzia la differenza di prezzo, non il prezzo totale
Usa la prova sociale (es. “Aggiornamento più popolare”)
Rispetta la scelta del cliente
Fornisci sempre un modo semplice per rifiutare
Non mostrare ripetutamente lo stesso upsell dopo un rifiuto
Tieni traccia e analizza quali upsell convertono per ottimizzare le offerte
Webhook da monitorare
Monitora questi eventi webhook per flussi di upsell e downgrade:
Evento Trigger Azione payment.succeededPagamento upsell/cross-sell completato Consegna prodotto, aggiorna accesso payment.failedAddebito con un clic fallito Mostra errore, offri riprovare o ripiego subscription.plan_changedUpgrade/downgrade completato Aggiorna funzionalità, invia conferma subscription.activeAbbonamento riattivato dopo cambiamento del piano Concedi accesso al nuovo livello
Guida all'integrazione dei webhook Scopri come configurare e verificare gli endpoint dei webhook.
Risorse correlate