Skip to main content

Recurring Payments & Subscriptions

Implement subscription billing, recurring payments, and automated scheduled charges using saved cards with Omise's Customers API and Schedules.

Overviewโ€‹

Recurring payments allow you to automatically charge customers on a regular schedule for subscriptions, memberships, SaaS products, or any service with repeat billing. Omise provides two approaches: manual recurring charges using the Customers API, or automated recurring charges using Schedules.

Key Features:

  • โœ… Saved cards - Store customer payment methods securely
  • โœ… Flexible schedules - Daily, weekly, monthly, yearly
  • โœ… Automated billing - Set-and-forget subscription management
  • โœ… Manual control - Charge customers programmatically
  • โœ… Retry logic - Automatic retry on failed payments
  • โœ… Prorated billing - Mid-cycle subscription changes
  • โœ… Multiple cards - Customers can save multiple payment methods

Approaches Comparisonโ€‹

FeatureCustomers API (Manual)Schedules (Automated)
ControlFull controlAutomated
FlexibilityHighly flexibleFixed schedules
Retry logicYou implementBuilt-in
ComplexityMore codeLess code
Use caseCustom billingStandard subscriptions
ProrationYou calculateYou implement
Best forComplex billing rulesSimple recurring charges

How It Worksโ€‹

Customers API Approachโ€‹

Schedules Approachโ€‹

Customers API (Manual Recurring)โ€‹

Step 1: Create Customer with Cardโ€‹

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

// Create customer with initial card
const customer = await omise.customers.create({
email: 'user@example.com',
description: 'John Doe - Premium Plan',
card: tokenId, // From Omise.js or mobile 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);

Step 2: Store Customer IDโ€‹

// Store in your database
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
});

Step 3: Charge Customer Recurringโ€‹

// Scheduled job (runs daily)
async function processRecurringBilling() {
const today = new Date().toDateString();

// Find customers due for billing
const dueCustomers = await db.users.find({
subscription_status: 'active',
next_billing_date: today
});

for (const user of dueCustomers) {
try {
// Charge customer's saved card
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') {
// Update subscription
await extendSubscription(user.user_id);
await sendReceiptEmail(user.email, charge);

console.log(`โœ“ Charged ${user.email}: ${charge.amount / 100}`);
} else {
// Handle failure
await handleFailedPayment(user, charge);
}

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

// Schedule to run daily
// Using node-cron or your scheduler
cron.schedule('0 0 * * *', processRecurringBilling);

Step 4: Handle Failed Paymentsโ€‹

async function handleFailedPayment(user, charge) {
// Increment retry count
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
});

// Retry logic
if (retryCount <= 3) {
// Retry after 3, 5, 7 days
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()
});

// Email customer
await sendPaymentFailedEmail(user.email, {
reason: charge.failure_message,
retryDate: retryDate,
updateCardUrl: `https://yoursite.com/billing/update-card`
});

} else {
// Suspend subscription after 3 retries
await db.users.update({
user_id: user.user_id,
subscription_status: 'suspended',
suspended_at: new Date()
});

await sendSubscriptionSuspendedEmail(user.email);
}
}

Step 5: Update Cardโ€‹

// Customer updates their card
app.post('/billing/update-card', async (req, res) => {
const { userId, newTokenId } = req.body;

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

// Update customer's default card
const customer = await omise.customers.update(user.omise_customer_id, {
card: newTokenId
});

// Reset retry count
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 (Automated Recurring)โ€‹

Step 1: Create Customerโ€‹

// Create customer first
const customer = await omise.customers.create({
email: 'user@example.com',
description: 'John Doe',
card: tokenId
});

Step 2: Create Scheduleโ€‹

// Create monthly recurring schedule
const schedule = await omise.schedules.create({
every: 1,
period: 'month',
start_date: '2025-02-15', // First charge date
end_date: '2026-02-15', // Optional: schedule end date
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]);

Period Optionsโ€‹

// Daily
const dailySchedule = await omise.schedules.create({
every: 1,
period: 'day',
start_date: '2025-02-10',
charge: { /* charge params */ }
});

// Weekly
const weeklySchedule = await omise.schedules.create({
every: 1,
period: 'week',
on: { weekdays: ['monday'] }, // Charge every Monday
start_date: '2025-02-10',
charge: { /* charge params */ }
});

// Monthly (specific day)
const monthlySchedule = await omise.schedules.create({
every: 1,
period: 'month',
on: { days_of_month: [1] }, // 1st of every month
start_date: '2025-02-01',
charge: { /* charge params */ }
});

// Yearly
const yearlySchedule = await omise.schedules.create({
every: 1,
period: 'year',
start_date: '2025-02-15',
charge: { /* charge params */ }
});

Step 3: Handle 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':
// Schedule suspended due to failed charges
handleScheduleSuspended(event.data);
break;

case 'schedule.expiring':
// Schedule about to expire
handleScheduleExpiring(event.data);
break;
}

res.sendStatus(200);
});

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

if (charge.status === 'successful') {
// Extend user's subscription
await extendSubscription(charge.customer);
await sendReceiptEmail(charge.customer);
} else {
// Omise automatically retries, but notify user
await sendPaymentIssueEmail(charge.customer);
}
}
}

Step 4: Manage Schedulesโ€‹

// Pause schedule
await omise.schedules.update('schd_test_...', {
status: 'suspended'
});

// Resume schedule
await omise.schedules.update('schd_test_...', {
status: 'active'
});

// Cancel schedule
await omise.schedules.destroy('schd_test_...');

Complete Subscription System Exampleโ€‹

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 plans
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'] }
};

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

// Validate plan
if (!PLANS[plan]) {
return res.status(400).json({ error: 'Invalid plan' });
}

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

// Initial charge
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') {
// Save 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 });
}
});

// Process recurring billing (runs daily at midnight)
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') {
// Update 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);
}
}
});

// Cancel subscription
app.post('/cancel-subscription', async (req, res) => {
const { userId } = req.body;

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

// Don't charge again, but let current period finish
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 });
}
});

// Utility functions
function addMonths(date, months) {
const result = new Date(date);
result.setMonth(result.getMonth() + months);
return result;
}

app.listen(3000);

Best Practicesโ€‹

1. Dunning Managementโ€‹

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

// Schedule next retry
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
});

// Send dunning email
await sendDunningEmail(subscription.user_id, retry.message, {
retryDate: nextRetry,
failureReason: charge.failure_message
});

} else {
// Cancel after all retries
await cancelSubscription(subscription.id);
}
}

2. Prorationโ€‹

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;

// Calculate proration
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;

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

// Update subscription
await db.subscriptions.update({
subscription_id: sub.id,
plan: newPlan,
amount: newPlanAmount
});
}

3. Grace Periodsโ€‹

// Give 3 days grace after failed payment
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') {
// Suspend access
await db.subscriptions.update({
subscription_id: subscription.id,
status: 'suspended'
});

await notifyUserSuspension(subscription.user_id);
}
}

4. Trial Periodsโ€‹

async function startTrial(userId, tokenId) {
// Create customer but don't charge
const customer = await omise.customers.create({
email: user.email,
card: tokenId
});

// Set trial end date
const trialEnd = new Date();
trialEnd.setDate(trialEnd.getDate() + 14); // 14-day trial

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

5. Email Notificationsโ€‹

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

Testingโ€‹

Test Mode Recurring Paymentsโ€‹

Test your subscription and recurring payment implementation using test cards:

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

// Create test customer with saved card
const customer = await omise.customers.create({
email: 'test.customer@example.com',
description: 'Test Subscriber',
card: 'tokn_test_5rt6s9vah5lkvi1rh9c' // Test card token
});

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

Test Scenariosโ€‹

1. Successful Recurring Chargeโ€‹

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

// First charge (signup)
const charge1 = await omise.charges.create({
amount: 29900,
currency: 'THB',
customer: customer.id,
description: 'First payment'
});

// Recurring charge (next month)
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. Failed Recurring Payment (Declined Card)โ€‹

// Create customer with decline card
const customer = await omise.customers.create({
email: 'test@example.com',
card: 'tokn_test_0002' // Decline card
});

// Attempt 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. Test Card Updateโ€‹

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

// Update to new card
const updated = await omise.customers.update(customer.id, {
card: 'tokn_test_new_card'
});

// Verify new card is default
console.log('Card updated:', updated.default_card !== customer.default_card);

4. Test Schedule (Automated Recurring)โ€‹

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

// Create monthly 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]);

// Simulate first charge in dashboard
// Dashboard โ†’ Schedules โ†’ Run Schedule

Test Cards for Subscriptionsโ€‹

Card NumberInitial ChargeRecurring ChargeUse Case
4242 4242 4242 4242SuccessSuccessHappy path
4000 0000 0000 0002DeclinedN/ASignup failure
4000 0000 0000 0010SuccessDeclinedRenewal failure (insufficient funds)
4242 4242 4242 4242 โ†’ UpdateSuccessSuccessCard update flow

Testing Saved Cardsโ€‹

// Test multiple cards
const customer = await omise.customers.create({
email: 'test@example.com'
});

// Add first card
const card1 = await omise.customers.update(customer.id, {
card: 'tokn_test_visa'
});

// Add second card
const card2 = await omise.customers.update(customer.id, {
card: 'tokn_test_mastercard'
});

// List customer's cards
const customerData = await omise.customers.retrieve(customer.id);
console.log('Number of cards:', customerData.cards.total);

// Charge specific card
const charge = await omise.charges.create({
amount: 29900,
currency: 'THB',
customer: customer.id,
card: card1.default_card // Use specific card
});

Testing via Dashboardโ€‹

Manual Recurring Chargesโ€‹

  1. Go to Test Dashboard โ†’ Customers
  2. Find your test customer
  3. View saved cards
  4. Click "Create Charge" to simulate recurring payment

Schedules Testingโ€‹

  1. Go to Test Dashboard โ†’ Schedules
  2. Find your schedule
  3. Click "Run Schedule" to trigger charge immediately
  4. Monitor Charges page for results

Test Webhooks for Subscriptionsโ€‹

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

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

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

case 'customer.update.card':
// Customer updated payment method
console.log('Card updated:', event.data.default_card);
break;

case 'schedule.suspend':
// Schedule suspended due to failed charges
console.log('Schedule suspended:', event.data.id);
notifyCustomerSuspension(event.data);
break;
}

res.sendStatus(200);
});

Testing Retry Logicโ€‹

// Simulate dunning management
async function testRetryFlow() {
const customer = await omise.customers.create({
email: 'test@example.com',
card: 'tokn_test_0010' // Insufficient funds
});

// Attempt 1: Initial charge fails
try {
await omise.charges.create({
amount: 29900,
currency: 'THB',
customer: customer.id
});
} catch (error) {
console.log('Attempt 1 failed:', error.code);
}

// Update to valid card
await omise.customers.update(customer.id, {
card: 'tokn_test_4242' // Success card
});

// Attempt 2: Should succeed
const retry = await omise.charges.create({
amount: 29900,
currency: 'THB',
customer: customer.id
});

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

Test Proration Logicโ€‹

// Test subscription upgrade with proration
async function testProration() {
const subscription = {
plan: 'basic',
amount: 9900,
started: new Date('2025-02-01'),
next_billing: new Date('2025-03-01')
};

// Calculate prorated upgrade
const daysInMonth = 30;
const daysUsed = 10; // 10 days into billing cycle
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);

// Charge prorated amount
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');
}

Testing Error Handlingโ€‹

// Test: Charge with deleted customer
try {
await omise.charges.create({
amount: 29900,
currency: 'THB',
customer: 'cust_test_deleted'
});
} catch (error) {
console.log('Expected error:', error.code); // 'not_found'
}

// Test: Charge customer with no cards
try {
const customer = await omise.customers.create({
email: 'test@example.com'
// No card attached
});

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

FAQโ€‹

Should I use Customers API or Schedules?
  • Use Customers API if you need custom billing logic, proration, metered billing, or complex pricing
  • Use Schedules for simple fixed-amount recurring charges with standard intervals
How do I handle failed subscription payments?

Implement dunning management:

  1. Retry failed payments automatically (3, 7, 14 days)
  2. Send reminder emails to update card
  3. Provide grace period (3-7 days)
  4. Suspend subscription after final retry
  5. Allow easy reactivation
Can customers have multiple subscriptions?

Yes, create separate subscription records in your database, each linked to the same customer_id in Omise. Charge each subscription independently.

How do I implement annual billing?
// Charge once per year
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 annual
});
What about metered billing (usage-based)?

Track usage throughout the billing period, then charge at the end:

// Track usage
await db.usage.increment({
user_id: userId,
api_calls: 1000
});

// At end of period
const usage = await db.usage.get(userId);
const amount = usage.api_calls * 10; // เธฟ0.10 per call

await omise.charges.create({
amount: amount,
customer: customerId
});
How do I handle card expiration?

Send proactive emails:

// Check cards expiring within 30 days
const expiringCards = await db.subscriptions.find({
card_expiry_month: nextMonth.getMonth() + 1,
card_expiry_year: nextMonth.getFullYear()
});

// Email customers
for (const sub of expiringCards) {
await sendCardExpiringEmail(sub.user_id);
}

Next Stepsโ€‹

  1. Create customer with card
  2. Implement billing logic
  3. Set up retry mechanism
  4. Configure webhooks
  5. Test subscription flow
  6. Go live