メインコンテンツへスキップ

Recurring PaymentsとSubscriptions

Omiseの Customers APIとSchedulesを使用して、保存されたカードでsubscription billing、recurring payments、自動scheduled chargesを実装します。

概要

Recurring paymentsを使用すると、subscriptions、memberships、SaaS製品、または繰り返し請求が必要なサービスに対して、定期的なスケジュールで顧客に自動的に課金できます。Omiseは2つのアプローチを提供しています: Customers APIを使用した手動recurring charges、またはSchedulesを使用した自動recurring chargesです。

主な機能:

  • 保存されたカード - 顧客の支払い方法を安全に保存
  • 柔軟なschedules - 日次、週次、月次、年次
  • 自動請求 - 設定後は放置できるsubscription管理
  • 手動コントロール - プログラムで顧客に課金
  • 再試行ロジック - 失敗した支払いの自動再試行
  • 日割り請求 - サイクル途中のsubscription変更
  • 複数のカード - 顧客は複数の支払い方法を保存可能

アプローチの比較

機能Customers API(手動)Schedules(自動)
コントロール完全なコントロール自動化
柔軟性非常に柔軟固定schedules
再試行ロジック自分で実装組み込み
複雑さより多くのコードより少ないコード
ユースケースカスタム請求標準subscriptions
日割り計算自分で計算自分で実装
最適な用途複雑な請求ルールシンプルなrecurring charges

仕組み

Customers APIアプローチ

Schedulesアプローチ

Customers API(手動Recurring)

ステップ1: カード付きでCustomerを作成

const omise = require('omise')({
secretKey: process.env.OMISE_SECRET_KEY
});

// 初期カード付きでcustomerを作成
const customer = await omise.customers.create({
email: 'user@example.com',
description: 'John Doe - Premium Plan',
card: tokenId, // Omise.jsまたはモバイルSDKから
metadata: {
plan: 'premium',
billing_cycle: 'monthly',
signup_date: new Date().toISOString()
}
});

console.log('Customer ID:', customer.id);
console.log('Default card:', customer.default_card);

ステップ2: Customer IDを保存

// データベースに保存
await db.users.update({
user_id: userId,
omise_customer_id: customer.id,
subscription_status: 'active',
subscription_plan: 'premium',
next_billing_date: calculateNextBillingDate(),
amount: 29900 // ฿299.00
});

ステップ3: Customerに定期課金

// スケジュールされたジョブ(毎日実行)
async function processRecurringBilling() {
const today = new Date().toDateString();

// 請求期限の顧客を検索
const dueCustomers = await db.users.find({
subscription_status: 'active',
next_billing_date: today
});

for (const user of dueCustomers) {
try {
// 顧客の保存されたカードに課金
const charge = await omise.charges.create({
amount: user.amount,
currency: 'THB',
customer: user.omise_customer_id,
description: `Subscription renewal - ${user.subscription_plan}`,
metadata: {
user_id: user.user_id,
plan: user.subscription_plan,
billing_period: today
}
});

if (charge.status === 'successful') {
// Subscriptionを更新
await extendSubscription(user.user_id);
await sendReceiptEmail(user.email, charge);

console.log(`✓ Charged ${user.email}: ${charge.amount / 100}`);
} else {
// 失敗を処理
await handleFailedPayment(user, charge);
}

} catch (error) {
console.error(`✗ Failed to charge ${user.email}:`, error.message);
await handlePaymentError(user, error);
}
}
}

// 毎日実行するようにスケジュール
// node-cronまたはスケジューラーを使用
cron.schedule('0 0 * * *', processRecurringBilling);

ステップ4: 失敗した支払いを処理

async function handleFailedPayment(user, charge) {
// 再試行カウントを増やす
const retryCount = (user.payment_retry_count || 0) + 1;

await db.users.update({
user_id: user.user_id,
payment_retry_count: retryCount,
last_payment_attempt: new Date(),
last_payment_error: charge.failure_message
});

// 再試行ロジック
if (retryCount <= 3) {
// 3、5、7日後に再試行
const retryDays = [3, 5, 7][retryCount - 1];
const retryDate = new Date();
retryDate.setDate(retryDate.getDate() + retryDays);

await db.users.update({
user_id: user.user_id,
next_billing_date: retryDate.toDateString()
});

// 顧客にメール送信
await sendPaymentFailedEmail(user.email, {
reason: charge.failure_message,
retryDate: retryDate,
updateCardUrl: `https://yoursite.com/billing/update-card`
});

} else {
// 3回の再試行後にsubscriptionを一時停止
await db.users.update({
user_id: user.user_id,
subscription_status: 'suspended',
suspended_at: new Date()
});

await sendSubscriptionSuspendedEmail(user.email);
}
}

ステップ5: カードを更新

// 顧客がカードを更新
app.post('/billing/update-card', async (req, res) => {
const { userId, newTokenId } = req.body;

try {
const user = await db.users.findOne({ user_id: userId });

// 顧客のデフォルトカードを更新
const customer = await omise.customers.update(user.omise_customer_id, {
card: newTokenId
});

// 再試行カウントをリセット
await db.users.update({
user_id: userId,
payment_retry_count: 0,
subscription_status: 'active'
});

res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});

Schedules(自動Recurring)

ステップ1: Customerを作成

// まずcustomerを作成
const customer = await omise.customers.create({
email: 'user@example.com',
description: 'John Doe',
card: tokenId
});

ステップ2: Scheduleを作成

// 月次recurring scheduleを作成
const schedule = await omise.schedules.create({
every: 1,
period: 'month',
start_date: '2025-02-15', // 最初の課金日
end_date: '2026-02-15', // オプション: schedule終了日
charge: {
customer: customer.id,
amount: 29900,
currency: 'THB',
description: 'Monthly subscription - Premium Plan'
}
});

console.log('Schedule ID:', schedule.id);
console.log('Next occurrence:', schedule.next_occurrence_dates[0]);

期間オプション

// 日次
const dailySchedule = await omise.schedules.create({
every: 1,
period: 'day',
start_date: '2025-02-10',
charge: { /* chargeパラメータ */ }
});

// 週次
const weeklySchedule = await omise.schedules.create({
every: 1,
period: 'week',
on: { weekdays: ['monday'] }, // 毎週月曜日に課金
start_date: '2025-02-10',
charge: { /* chargeパラメータ */ }
});

// 月次(特定の日)
const monthlySchedule = await omise.schedules.create({
every: 1,
period: 'month',
on: { days_of_month: [1] }, // 毎月1日
start_date: '2025-02-01',
charge: { /* chargeパラメータ */ }
});

// 年次
const yearlySchedule = await omise.schedules.create({
every: 1,
period: 'year',
start_date: '2025-02-15',
charge: { /* chargeパラメータ */ }
});

ステップ3: Schedule Webhooksを処理

app.post('/webhooks/omise', (req, res) => {
const event = req.body;

switch (event.key) {
case 'charge.complete':
handleScheduledCharge(event.data);
break;

case 'schedule.suspend':
// 失敗したchargesによりscheduleが一時停止
handleScheduleSuspended(event.data);
break;

case 'schedule.expiring':
// Scheduleがまもなく期限切れ
handleScheduleExpiring(event.data);
break;
}

res.sendStatus(200);
});

async function handleScheduledCharge(charge) {
if (charge.schedule) {
console.log('Schedule charge:', charge.schedule);

if (charge.status === 'successful') {
// ユーザーのsubscriptionを延長
await extendSubscription(charge.customer);
await sendReceiptEmail(charge.customer);
} else {
// Omiseは自動的に再試行しますが、ユーザーに通知
await sendPaymentIssueEmail(charge.customer);
}
}
}

ステップ4: Schedulesを管理

// Scheduleを一時停止
await omise.schedules.update('schd_test_...', {
status: 'suspended'
});

// Scheduleを再開
await omise.schedules.update('schd_test_...', {
status: 'active'
});

// Scheduleをキャンセル
await omise.schedules.destroy('schd_test_...');

完全なSubscriptionシステムの例

const express = require('express');
const cron = require('node-cron');
const omise = require('omise')({
secretKey: process.env.OMISE_SECRET_KEY
});

const app = express();
app.use(express.json());

// Subscriptionプラン
const PLANS = {
basic: { amount: 9900, name: 'Basic', features: ['Feature A'] },
premium: { amount: 29900, name: 'Premium', features: ['Feature A', 'Feature B'] },
enterprise: { amount: 99900, name: 'Enterprise', features: ['All features'] }
};

// Subscriptionを作成
app.post('/subscribe', async (req, res) => {
try {
const { userId, plan, tokenId, email } = req.body;

// プランを検証
if (!PLANS[plan]) {
return res.status(400).json({ error: 'Invalid plan' });
}

// Customerを作成
const customer = await omise.customers.create({
email: email,
description: `User ${userId} - ${PLANS[plan].name}`,
card: tokenId,
metadata: {
user_id: userId,
plan: plan
}
});

// 初回課金
const charge = await omise.charges.create({
amount: PLANS[plan].amount,
currency: 'THB',
customer: customer.id,
description: `${PLANS[plan].name} - First payment`
});

if (charge.status === 'successful') {
// Subscriptionを保存
await db.subscriptions.create({
user_id: userId,
customer_id: customer.id,
plan: plan,
status: 'active',
current_period_start: new Date(),
current_period_end: addMonths(new Date(), 1),
amount: PLANS[plan].amount,
next_billing_date: addMonths(new Date(), 1)
});

res.json({
success: true,
subscription: {
plan: PLANS[plan].name,
amount: PLANS[plan].amount / 100,
next_billing: addMonths(new Date(), 1)
}
});
} else {
res.status(400).json({ error: 'Payment failed' });
}

} catch (error) {
res.status(500).json({ error: error.message });
}
});

// Recurring billingを処理(深夜0時に毎日実行)
cron.schedule('0 0 * * *', async () => {
console.log('Processing recurring billing...');

const today = new Date().toDateString();
const dueSubscriptions = await db.subscriptions.find({
status: 'active',
next_billing_date: today
});

for (const sub of dueSubscriptions) {
try {
const charge = await omise.charges.create({
amount: sub.amount,
currency: 'THB',
customer: sub.customer_id,
description: `${sub.plan} - Monthly subscription`
});

if (charge.status === 'successful') {
// Subscriptionを更新
await db.subscriptions.update({
subscription_id: sub.id,
current_period_start: new Date(),
current_period_end: addMonths(new Date(), 1),
next_billing_date: addMonths(new Date(), 1),
retry_count: 0
});

await sendReceiptEmail(sub.user_id, charge);
console.log(`✓ Billed user ${sub.user_id}`);
} else {
await handleFailedBilling(sub, charge);
}

} catch (error) {
console.error(`✗ Error billing user ${sub.user_id}:`, error);
await handleBillingError(sub, error);
}
}
});

// Subscriptionをキャンセル
app.post('/cancel-subscription', async (req, res) => {
const { userId } = req.body;

try {
const sub = await db.subscriptions.findOne({
user_id: userId,
status: 'active'
});

// 再度課金せず、現在の期間を終了させる
await db.subscriptions.update({
subscription_id: sub.id,
status: 'canceled',
canceled_at: new Date(),
access_until: sub.current_period_end
});

res.json({
success: true,
message: 'Subscription canceled',
access_until: sub.current_period_end
});

} catch (error) {
res.status(500).json({ error: error.message });
}
});

// ユーティリティ関数
function addMonths(date, months) {
const result = new Date(date);
result.setMonth(result.getMonth() + months);
return result;
}

app.listen(3000);

ベストプラクティス

1. Dunning管理

const RETRY_SCHEDULE = [
{ day: 3, message: 'first_reminder' },
{ day: 7, message: 'second_reminder' },
{ day: 14, message: 'final_notice' }
];

async function handleFailedBilling(subscription, charge) {
const retryCount = subscription.retry_count || 0;

if (retryCount < RETRY_SCHEDULE.length) {
const retry = RETRY_SCHEDULE[retryCount];

// 次の再試行をスケジュール
const nextRetry = new Date();
nextRetry.setDate(nextRetry.getDate() + retry.day);

await db.subscriptions.update({
subscription_id: subscription.id,
retry_count: retryCount + 1,
next_billing_date: nextRetry
});

// Dunningメールを送信
await sendDunningEmail(subscription.user_id, retry.message, {
retryDate: nextRetry,
failureReason: charge.failure_message
});

} else {
// すべての再試行後にキャンセル
await cancelSubscription(subscription.id);
}
}

2. 日割り計算

async function upgradeSubscription(userId, newPlan) {
const sub = await db.subscriptions.findOne({ user_id: userId });
const oldPlanAmount = PLANS[sub.plan].amount;
const newPlanAmount = PLANS[newPlan].amount;

// 日割り計算
const daysInMonth = 30;
const daysRemaining = Math.ceil(
(sub.current_period_end - new Date()) / (1000 * 60 * 60 * 24)
);
const proratedRefund = (oldPlanAmount / daysInMonth) * daysRemaining;
const proratedCharge = (newPlanAmount / daysInMonth) * daysRemaining;
const amountDue = proratedCharge - proratedRefund;

// 差額を課金
if (amountDue > 0) {
const charge = await omise.charges.create({
amount: Math.round(amountDue),
currency: 'THB',
customer: sub.customer_id,
description: `Upgrade to ${newPlan} (prorated)`
});
}

// Subscriptionを更新
await db.subscriptions.update({
subscription_id: sub.id,
plan: newPlan,
amount: newPlanAmount
});
}

3. 猶予期間

// 支払い失敗後に3日間の猶予を与える
async function checkGracePeriod(subscription) {
const daysSinceFailure = Math.floor(
(new Date() - subscription.last_payment_attempt) / (1000 * 60 * 60 * 24)
);

if (daysSinceFailure > 3 && subscription.status === 'past_due') {
// アクセスを一時停止
await db.subscriptions.update({
subscription_id: subscription.id,
status: 'suspended'
});

await notifyUserSuspension(subscription.user_id);
}
}

4. トライアル期間

async function startTrial(userId, tokenId) {
// Customerを作成するが課金しない
const customer = await omise.customers.create({
email: user.email,
card: tokenId
});

// トライアル終了日を設定
const trialEnd = new Date();
trialEnd.setDate(trialEnd.getDate() + 14); // 14日間のトライアル

await db.subscriptions.create({
user_id: userId,
customer_id: customer.id,
status: 'trialing',
trial_end: trialEnd,
next_billing_date: trialEnd
});
}

5. メール通知

const EMAIL_TEMPLATES = {
receipt: 'Your payment was successful',
failed: 'Payment failed - please update your card',
upcoming: 'Your subscription renews in 3 days',
canceled: 'Your subscription has been canceled',
suspended: 'Your subscription is suspended'
};

async function sendSubscriptionEmail(userId, type, data) {
const user = await db.users.findOne({ user_id: userId });

await sendEmail({
to: user.email,
subject: EMAIL_TEMPLATES[type],
template: type,
data: data
});
}

テスト

テストモードでのRecurring Payments

テストカードを使用してsubscriptionとrecurring paymentの実装をテストします:

const omise = require('omise')({
secretKey: 'skey_test_YOUR_SECRET_KEY'
});

// 保存されたカード付きでテストcustomerを作成
const customer = await omise.customers.create({
email: 'test.customer@example.com',
description: 'Test Subscriber',
card: 'tokn_test_5rt6s9vah5lkvi1rh9c' // テストカード token
});

console.log('Test customer ID:', customer.id);

テストシナリオ

1. 成功したRecurring Charge

// Customerを作成
const customer = await omise.customers.create({
email: 'test@example.com',
card: 'tokn_test_4242' // 成功カード
});

// 最初のcharge(サインアップ)
const charge1 = await omise.charges.create({
amount: 29900,
currency: 'THB',
customer: customer.id,
description: 'First payment'
});

// Recurring charge(翌月)
const charge2 = await omise.charges.create({
amount: 29900,
currency: 'THB',
customer: customer.id,
description: 'Monthly renewal'
});

console.log('Both charges successful:',
charge1.status === 'successful' &&
charge2.status === 'successful'
);

2. 失敗したRecurring Payment(拒否されたカード)

// 拒否カード付きでcustomerを作成
const customer = await omise.customers.create({
email: 'test@example.com',
card: 'tokn_test_0002' // 拒否カード
});

// Recurring chargeを試行
try {
const charge = await omise.charges.create({
amount: 29900,
currency: 'THB',
customer: customer.id
});
} catch (error) {
console.log('Payment failed:', error.code); // 'payment_rejected'
console.log('Implement retry logic here');
}

3. テストカード更新

// 顧客が期限切れカードを更新
const customer = await omise.customers.create({
email: 'test@example.com',
card: 'tokn_test_4242'
});

// 新しいカードに更新
const updated = await omise.customers.update(customer.id, {
card: 'tokn_test_new_card'
});

// 新しいカードがデフォルトであることを確認
console.log('Card updated:', updated.default_card !== customer.default_card);

4. テストSchedule(自動Recurring)

// Customerを作成
const customer = await omise.customers.create({
email: 'test@example.com',
card: 'tokn_test_4242'
});

// 月次scheduleを作成
const schedule = await omise.schedules.create({
every: 1,
period: 'month',
start_date: '2025-02-15',
charge: {
customer: customer.id,
amount: 29900,
currency: 'THB',
description: 'Test subscription'
}
});

console.log('Schedule created:', schedule.id);
console.log('Next charge:', schedule.next_occurrence_dates[0]);

// ダッシュボードで最初のchargeをシミュレート
// Dashboard → Schedules → Run Schedule

Subscriptions用テストカード

カード番号初回ChargeRecurring Chargeユースケース
4242 4242 4242 4242成功成功ハッピーパス
4000 0000 0000 0002拒否N/Aサインアップ失敗
4000 0000 0000 0010成功拒否更新失敗(残高不足)
4242 4242 4242 4242 → 更新成功成功カード更新フロー

保存されたカードのテスト

// 複数のカードをテスト
const customer = await omise.customers.create({
email: 'test@example.com'
});

// 最初のカードを追加
const card1 = await omise.customers.update(customer.id, {
card: 'tokn_test_visa'
});

// 2番目のカードを追加
const card2 = await omise.customers.update(customer.id, {
card: 'tokn_test_mastercard'
});

// 顧客のカードをリスト
const customerData = await omise.customers.retrieve(customer.id);
console.log('Number of cards:', customerData.cards.total);

// 特定のカードに課金
const charge = await omise.charges.create({
amount: 29900,
currency: 'THB',
customer: customer.id,
card: card1.default_card // 特定のカードを使用
});

ダッシュボードでのテスト

手動Recurring Charges

  1. Test Dashboard → Customersに移動
  2. テストcustomerを検索
  3. 保存されたカードを表示
  4. **"Create Charge"**をクリックしてrecurring paymentをシミュレート

Schedulesテスト

  1. Test Dashboard → Schedulesに移動
  2. Scheduleを検索
  3. **"Run Schedule"**をクリックして即座にchargeをトリガー
  4. Chargesページで結果を監視

Subscriptions用テストWebhooks

app.post('/webhooks/omise', (req, res) => {
const event = req.body;

switch (event.key) {
case 'charge.complete':
// Recurring payment成功
if (event.data.schedule) {
console.log('Schedule charge:', event.data.schedule);
}
handleSuccessfulRenewal(event.data);
break;

case 'charge.failed':
// Recurring payment失敗
console.log('Failure reason:', event.data.failure_message);
handleFailedRenewal(event.data);
break;

case 'customer.update.card':
// Customerが支払い方法を更新
console.log('Card updated:', event.data.default_card);
break;

case 'schedule.suspend':
// 失敗したchargesによりscheduleが一時停止
console.log('Schedule suspended:', event.data.id);
notifyCustomerSuspension(event.data);
break;
}

res.sendStatus(200);
});

再試行ロジックのテスト

// Dunning管理をシミュレート
async function testRetryFlow() {
const customer = await omise.customers.create({
email: 'test@example.com',
card: 'tokn_test_0010' // 残高不足
});

// 試行1: 初回chargeが失敗
try {
await omise.charges.create({
amount: 29900,
currency: 'THB',
customer: customer.id
});
} catch (error) {
console.log('Attempt 1 failed:', error.code);
}

// 有効なカードに更新
await omise.customers.update(customer.id, {
card: 'tokn_test_4242' // 成功カード
});

// 試行2: 成功するはず
const retry = await omise.charges.create({
amount: 29900,
currency: 'THB',
customer: customer.id
});

console.log('Retry successful:', retry.status === 'successful');
}

日割りロジックのテスト

// 日割りを伴うsubscriptionアップグレードをテスト
async function testProration() {
const subscription = {
plan: 'basic',
amount: 9900,
started: new Date('2025-02-01'),
next_billing: new Date('2025-03-01')
};

// 日割りアップグレードを計算
const daysInMonth = 30;
const daysUsed = 10; // 請求サイクルの10日目
const daysRemaining = daysInMonth - daysUsed;

const oldPlanDaily = 9900 / daysInMonth;
const newPlanDaily = 29900 / daysInMonth;

const refund = oldPlanDaily * daysRemaining;
const charge = newPlanDaily * daysRemaining;
const amountDue = Math.round(charge - refund);

console.log('Prorated amount:', amountDue / 100);

// 日割り額を課金
const proratedCharge = await omise.charges.create({
amount: amountDue,
currency: 'THB',
customer: 'cust_test_...',
description: 'Upgrade to premium (prorated)'
});

console.log('Proration charged:', proratedCharge.status === 'successful');
}

エラー処理のテスト

// テスト: 削除されたcustomerでcharge
try {
await omise.charges.create({
amount: 29900,
currency: 'THB',
customer: 'cust_test_deleted'
});
} catch (error) {
console.log('Expected error:', error.code); // 'not_found'
}

// テスト: カードのないcustomerにcharge
try {
const customer = await omise.customers.create({
email: 'test@example.com'
// カードは添付されていない
});

await omise.charges.create({
amount: 29900,
currency: 'THB',
customer: customer.id
});
} catch (error) {
console.log('Expected error:', error.code); // 'invalid_card'
}

FAQ

Customers APIとSchedulesのどちらを使うべきですか?
  • Customers APIを使用: カスタム請求ロジック、日割り計算、従量課金、または複雑な価格設定が必要な場合
  • Schedulesを使用: 標準的な間隔での固定金額のシンプルなrecurring chargesの場合
失敗したsubscription paymentsをどのように処理しますか?

Dunning管理を実装:

  1. 失敗した支払いを自動的に再試行(3、7、14日)
  2. カード更新のリマインダーメールを送信
  3. 猶予期間を提供(3〜7日)
  4. 最終再試行後にsubscriptionを一時停止
  5. 簡単な再有効化を許可
顧客は複数のsubscriptionsを持つことができますか?

はい、データベースに個別のsubscriptionレコードを作成し、それぞれをOmiseの同じcustomer_idにリンクします。各subscriptionを独立して課金します。

年間請求をどのように実装しますか?
// 年に1回課金
const nextYear = new Date();
nextYear.setFullYear(nextYear.getFullYear() + 1);

await db.subscriptions.create({
user_id: userId,
billing_period: 'yearly',
next_billing_date: nextYear,
amount: 299900 // ฿2,999 年間
});
従量課金(使用量ベース)はどうですか?

請求期間中の使用量を追跡し、期間終了時に課金:

// 使用量を追跡
await db.usage.increment({
user_id: userId,
api_calls: 1000
});

// 期間終了時
const usage = await db.usage.get(userId);
const amount = usage.api_calls * 10; // 1コールあたり฿0.10

await omise.charges.create({
amount: amount,
customer: customerId
});
カードの有効期限をどのように処理しますか?

プロアクティブにメールを送信:

// 30日以内に期限切れになるカードを確認
const expiringCards = await db.subscriptions.find({
card_expiry_month: nextMonth.getMonth() + 1,
card_expiry_year: nextMonth.getFullYear()
});

// 顧客にメール送信
for (const sub of expiringCards) {
await sendCardExpiringEmail(sub.user_id);
}

関連リソース

次のステップ

  1. カード付きでcustomerを作成
  2. 請求ロジックを実装
  3. 再試行メカニズムを設定
  4. Webhooksを設定
  5. Subscriptionフローをテスト
  6. 本番環境に移行