メインコンテンツへスキップ
アップセルとダウンセルでは、保存済みの支払い方法を使って追加商品やプラン変更を顧客に提案できます。これにより支払い情報の再入力なしでワンクリック購入が可能になり、コンバージョン率が劇的に向上します。

Post-Purchase Upsells

チェックアウト直後に補完的な商品をワンクリックで提供します。

Subscription Upgrades

自動按分と即時請求で顧客を上位プランへ移行させます。

Cross-Sells

支払い情報を再入力せずに既存顧客に関連商品を追加します。

概要

アップセルとダウンセルは強力な収益最適化戦略です:
  • アップセル:より高価値の製品やアップグレード(例:BasicではなくProプラン)を提案
  • ダウンセル:顧客が断ったりダウングレードする際により低価格の代替案を提示
  • クロスセル:補完的な商品(例:アドオン、関連アイテム)を提案
Dodo Paymentsはpayment_method_idパラメータを通じてこれらのフローを可能にし、顧客がカード情報を再入力することなく保存済みの支払い方法に課金できます。

主な利点

利点影響
ワンクリック購入リピーターは支払いフォームを完全にスキップ
コンバージョン増加意思決定の瞬間での摩擦を減らす
即時処理confirm: trueで課金がすぐに処理される
シームレスなUX顧客はフローを通じてアプリ内に留まる

仕組み

前提条件

アップセル・ダウンセルを実装する前に以下を確認してください:
1

Customer with Saved Payment Method

顧客は少なくとも1回の購入を完了している必要があります。チェックアウトを完了すると支払い方法は自動的に保存されます。
2

Products Configured

Dodo Paymentsダッシュボードでアップセル商品を作成してください。これは一回限りの支払い、サブスクリプション、またはアドオンで構いません。
3

Webhook Endpoint

payment.succeededpayment.failed、およびsubscription.plan_changedイベントを処理するためのWebhookを設定してください。

顧客の支払い方法を取得する

アップセルを提供する前に、顧客の保存済み支払い方法を取得してください:
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 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;
}
payment_method_idを使用する場合は、confirm: trueを設定し、既存のcustomer_idを提供する必要があります。支払い方法はその顧客に属していなければなりません。

サブスクリプションのアップグレード

自動按分処理で、顧客を上位のサブスクリプションプランへ移行させます。

適用前のプレビュー

常にプラン変更をプレビューして、顧客に請求内容を正確に示してください:
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}`);

アップグレードを実行

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);

按分モード

顧客への請求方法を選択してください:
モード挙動適した用途
difference_immediately価格差を即時請求(3030→80 = $50)シンプルなアップグレード
prorated_immediately請求期間の残り時間に基づいて課金利用時間に応じた請求
full_immediately新しいプランの全額を請求し、残期間を無視請求サイクルのリセット
単純なアップグレードフローにはdifference_immediatelyを使用してください。現在のプランの未使用時間を考慮したい場合はprorated_immediatelyを利用します。

クロスセル

支払い情報の再入力なしに既存顧客へ補完的な商品を追加してください。

実装

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);
}

サブスクリプションのダウングレード

顧客が下位プランへ移行する際には、自動クレジットでスムーズに対応します。

ダウングレードの仕組み

  1. 顧客がダウングレードを要求(Pro → Basic)
  2. 現在のプランの残存価値を計算
  3. 将来の更新に向けてサブスクリプションへクレジットを追加
  4. 顧客は即座に新プランへ移行
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');
difference_immediatelyを使用したダウングレードからのクレジットはサブスクリプション単位で、将来の更新に自動的に適用されます。Customer Creditsとは別のものです。

完全な例:購入後のアップセルフロー

成功した購入後にアップセルを提供する完全な実装例はこちらです:
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);

ベストプラクティス

アップセルを提供する最適なタイミングは、顧客が購入直後で購買モードにある瞬間です。その他の効果的なタイミング:
  • 機能の利用マイルストーン後
  • プランの制限に近づいたタイミング
  • オンボーディング完了時
ワンクリック課金を試みる前に、支払い方法を確認してください:
  • 商品の通貨に対応しているか
  • 有効期限が切れていないか
  • 顧客に紐づいているか
APIがこれらを検証しますが、事前に確認することでUXが向上します。
ワンクリック課金が失敗した際は:
  1. 標準のチェックアウトフローに戻す
  2. 明確なメッセージで顧客に通知
  3. 支払い方法の更新を促す
  4. 失敗した課金を繰り返し試みない
顧客が価値を理解しているとアップセルの成約率が上がります:
  • 現在のプランと比較して何が得られるかを示す
  • 総額ではなく価格差を強調
  • ソーシャルプルーフを活用(例:「最も人気のあるアップグレード」)
  • 断るための簡単な方法を常に提供する
  • 断られた後に同じアップセルを繰り返し表示しない
  • どのアップセルが成約しているかを追跡・分析して最適化する

監視すべきWebhook

アップセルやダウングレードのフローでこれらのWebhookイベントを追跡してください:
イベントトリガーアクション
payment.succeededアップセル/クロスセルの支払い完了商品を提供しアクセスを更新
payment.failedワンクリック課金が失敗エラー表示、再試行またはフォールバックを提案
subscription.plan_changedアップグレード/ダウングレード完了機能を更新し確認を送信
subscription.activeプラン変更後にサブスクリプションが再アクティブ化新しい階層へのアクセスを付与

Webhook Integration Guide

Webhookエンドポイントの設定と検証方法を学ぶ。

関連リソース