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โ
| Feature | Customers API (Manual) | Schedules (Automated) |
|---|---|---|
| Control | Full control | Automated |
| Flexibility | Highly flexible | Fixed schedules |
| Retry logic | You implement | Built-in |
| Complexity | More code | Less code |
| Use case | Custom billing | Standard subscriptions |
| Proration | You calculate | You implement |
| Best for | Complex billing rules | Simple 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 Number | Initial Charge | Recurring Charge | Use Case |
|---|---|---|---|
| 4242 4242 4242 4242 | Success | Success | Happy path |
| 4000 0000 0000 0002 | Declined | N/A | Signup failure |
| 4000 0000 0000 0010 | Success | Declined | Renewal failure (insufficient funds) |
| 4242 4242 4242 4242 โ Update | Success | Success | Card 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โ
- Go to Test Dashboard โ Customers
- Find your test customer
- View saved cards
- Click "Create Charge" to simulate recurring payment
Schedules Testingโ
- Go to Test Dashboard โ Schedules
- Find your schedule
- Click "Run Schedule" to trigger charge immediately
- 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:
- Retry failed payments automatically (3, 7, 14 days)
- Send reminder emails to update card
- Provide grace period (3-7 days)
- Suspend subscription after final retry
- 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);
}
Related Resourcesโ
- Saved Cards - Customers API details
- Webhooks - Handle subscription events
- Testing - Test subscriptions
- Refunds - Proration refunds
- 3D Secure - Note: Not recommended for subscriptions