Upsells e downsells permitem oferecer produtos adicionais ou mudanças de plano para os clientes usando seus métodos de pagamento salvos. Isso possibilita compras de um clique que ignoram a coleta de pagamento, melhorando drasticamente as taxas de conversão.
Post-Purchase Upsells
Ofereça produtos complementares imediatamente após o checkout com compras de um clique.
Subscription Upgrades
Mova clientes para níveis superiores com prorrateamento automático e faturamento instantâneo.
Cross-Sells
Adicione produtos relacionados a clientes existentes sem pedir novamente os dados de pagamento.
Visão geral
Upsells e downsells são estratégias poderosas de otimização de receita:- Upsells: Ofereça um produto de maior valor ou upgrade (ex.: plano Pro em vez de Basic)
- Downsells: Ofereça uma alternativa de menor preço quando o cliente recusar ou fizer downgrade
- Cross-sells: Sugira produtos complementares (ex.: add-ons, itens relacionados)
payment_method_id, que permite cobrar o método de pagamento salvo do cliente sem exigir que ele digite novamente os dados do cartão.
Benefícios principais
| Benefício | Impacto |
|---|---|
| Compras com um clique | Ignore o formulário de pagamento para clientes recorrentes |
| Maior conversão | Reduza o atrito no momento da decisão |
| Processamento instantâneo | As cobranças são processadas imediatamente com confirm: true |
| Experiência no aplicativo | Os clientes permanecem no seu aplicativo durante todo o fluxo |
Como funciona
Pré-requisitos
Antes de implementar upsells e downsells, garanta que você tenha:- Clientes com métodos de pagamento salvos (salvos automaticamente após a primeira compra)
- Produtos de upsell configurados no dashboard (pagamentos únicos, assinaturas ou complementos)
- Um endpoint de webhook configurado para processar eventos
payment.succeeded,payment.failedesubscription.plan_changed
Obtendo métodos de pagamento do cliente
Antes de oferecer um upsell, recupere os métodos de pagamento salvos do cliente:- 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)
}
}
Os métodos de pagamento são salvos automaticamente quando os clientes concluem o checkout. Não é necessário salvá-los explicitamente.
Upsells com um clique após a compra
Ofereça produtos adicionais imediatamente após uma compra bem-sucedida. O cliente pode aceitar com um único clique, pois o método de pagamento já está salvo.Implementação
- 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
}
Ao passar
payment_method_id para uma sessão de checkout, você também deve definir confirm: true e fornecer um customer_id existente. O método de pagamento deve pertencer a esse cliente. POST /payments não possui o campo confirm.Upgrades de assinatura
Mova os clientes para planos de assinatura de nível superior com tratamento automático de rateio proporcional.Visualização antes da confirmação
Sempre visualize as alterações do plano para mostrar aos clientes exatamente quanto será cobrado:- 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
}
Executar o upgrade
- 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")
}
Modos de rateio proporcional
Escolha como os clientes serão cobrados ao fazer upgrade:difference_immediately
Cobra instantaneamente a diferença de preço ($30→$80 = $50). Ideal para upgrades simples.
prorated_immediately
Concede crédito pelo tempo não utilizado do plano antigo e cobra um ciclo completo do novo plano. Ideal para creditar o tempo não utilizado.
full_immediately
Cobra o preço total do novo plano e ignora o tempo restante. Ideal para redefinir o ciclo de cobrança.
do_not_bill
Aplica a alteração do plano sem cobrança imediata; o novo plano será cobrado na próxima renovação e a data de cobrança original será preservada. Ideal para upgrades de cortesia e migrações gratuitas.
Use
difference_immediately para fluxos de upgrade simples — a cobrança corresponde apenas à diferença de preço. Use prorated_immediately quando quiser conceder ao cliente um crédito pelo tempo não utilizado do plano atual e cobrar um ciclo completo do novo plano.Vendas cruzadas
Adicione produtos complementares para clientes existentes sem exigir que eles insiram novamente os dados de pagamento.Implementação
- 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)
}
Downgrades de assinatura
Quando os clientes quiserem mudar para um plano de nível inferior, conduza a transição de forma adequada com créditos automáticos.Como funcionam os downgrades
- O cliente solicita o downgrade (Pro → Basic)
- O sistema calcula o valor restante do plano atual
- O crédito é adicionado à assinatura para futuras renovações
- O cliente muda imediatamente para o novo plano
O
immediate_charge.summary.customer_credits da prévia está na moeda da carteira de créditos do cliente, indicada por customer_credits_currency. Ela pode ser diferente da currency do resumo, por exemplo, quando o cliente paga em INR por uma assinatura em USD.- 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
}
Os créditos provenientes de downgrades que usam
difference_immediately têm escopo de assinatura e são aplicados automaticamente a renovações futuras. Eles são diferentes dos benefícios de Credit-Based Billing.Exemplo completo: fluxo de upsell após a compra
Veja uma implementação completa que mostra como oferecer um upsell após uma compra bem-sucedida:- 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)
Práticas recomendadas
- Escolha o momento estrategicamente: ofereça upsells imediatamente após uma compra bem-sucedida, quando os clientes estão com mentalidade de compra. Outros momentos eficazes: após marcos de uso de recursos, ao se aproximarem dos limites do plano e durante a conclusão da integração.
- Valide os métodos de pagamento: antes de tentar uma cobrança com um clique, verifique se o método de pagamento é compatível com a moeda do produto, não expirou e pertence ao cliente.
- Trate as falhas de forma adequada: quando as cobranças com um clique falharem, recorra ao fluxo de checkout padrão, notifique o cliente com uma mensagem clara e ofereça a opção de atualizar o método de pagamento.
- Mostre o valor com clareza: mostre o que os clientes receberão em comparação com o plano atual, destaque a diferença de preço (não o preço total) e use prova social.
- Respeite a escolha do cliente: sempre ofereça uma maneira fácil de recusar, não mostre o mesmo upsell repetidamente após uma recusa e acompanhe quais upsells convertem para otimizar as ofertas.
Webhooks para monitorar
Acompanhe estes eventos de webhook para os fluxos de upsell e downgrade:| Evento | Gatilho | Ação |
|---|---|---|
payment.succeeded | Pagamento de upsell/cross-sell concluído | Entregar o produto, atualizar o acesso |
payment.failed | Falha na cobrança com um clique | Mostrar o erro, oferecer nova tentativa ou alternativa |
subscription.plan_changed | Upgrade/downgrade concluído | Atualizar os recursos, enviar confirmação |
subscription.active | Assinatura reativada após alteração de plano | Conceder acesso ao novo nível |
Webhook Integration Guide
Saiba como configurar e verificar endpoints de webhook.
Recursos relacionados
Subscription Upgrade Guide
Guia detalhado sobre alterações de plano, modos de proration e tratamento de falhas.
Checkout Sessions
Referência completa para criar sessões de checkout com todas as opções.
Customer Payment Methods API
Referência da API para listar os métodos de pagamento do cliente.
Add-ons
Aprimore as assinaturas com add-ons flexíveis para gerar receita adicional.