Saved Payment Methods
Learn how to save and manage payment methods securely for recurring billing, one-click checkout experiences, and subscription services. This guide covers card storage, management, and charging saved payment methods.
Overviewโ
Saved payment methods enable you to charge customers without collecting payment details for each transaction. This is essential for:
- Recurring subscription billing
- One-click checkout experiences
- Auto-renewal services
- Membership platforms
- Usage-based billing
Key Benefitsโ
- Improved Conversion - Reduce friction with one-click payments
- Better Retention - Seamless subscription renewals
- Reduced PCI Scope - Tokens instead of raw card data
- Multiple Payment Options - Support multiple saved cards
- Enhanced Security - 3D Secure on first use, faster subsequent charges
Security & Complianceโ
Omise handles all card storage securely:
- Cards are tokenized and encrypted
- PCI DSS Level 1 compliant infrastructure
- No raw card data touches your servers
- Secure vault for card storage
- Compliance with data protection regulations
Saving Payment Methodsโ
Save Card with Customer Creationโ
const omise = require('omise')({
secretKey: 'skey_test_123'
});
// Create customer with card
const customer = await omise.customers.create({
email: 'john@example.com',
description: 'John Doe',
card: 'tokn_test_123456' // Token from client-side
});
console.log('Customer ID:', customer.id);
console.log('Default Card:', customer.default_card);
import omise
omise.api_secret = 'skey_test_123'
# Create customer with card
customer = omise.Customer.create(
email='john@example.com',
description='John Doe',
card='tokn_test_123456'
)
print(f'Customer ID: {customer.id}')
print(f'Default Card: {customer.default_card}')
<?php
$omise = new Omise(['secretKey' => 'skey_test_123']);
$customer = $omise['customers']->create([
'email' => 'john@example.com',
'description' => 'John Doe',
'card' => 'tokn_test_123456'
]);
echo "Customer ID: " . $customer['id'] . "\n";
echo "Default Card: " . $customer['default_card'];
curl https://api.omise.co/customers \
-u skey_test_123: \
-d "email=john@example.com" \
-d "description=John Doe" \
-d "card=tokn_test_123456"
Add Card to Existing Customerโ
// Add additional card
await omise.customers.update('cust_test_123456', {
card: 'tokn_test_new_card'
});
// Retrieve updated customer
const customer = await omise.customers.retrieve('cust_test_123456');
console.log(`Total cards: ${customer.cards.total}`);
console.log('Cards:', customer.cards.data.map(c =>
`${c.brand} โขโขโขโข ${c.last_digits}`
));
# Add additional card
customer = omise.Customer.retrieve('cust_test_123456')
customer.update(card='tokn_test_new_card')
# Retrieve updated customer
customer.reload()
print(f'Total cards: {customer.cards.total}')
for card in customer.cards.data:
print(f'{card.brand} โขโขโขโข {card.last_digits}')
curl https://api.omise.co/customers/cust_test_123456 \
-u skey_test_123: \
-d "card=tokn_test_new_card"
Client-Side Card Saving Flowโ
// Frontend: Create token
async function saveCard() {
const cardData = {
name: document.getElementById('name').value,
number: document.getElementById('number').value,
expiration_month: document.getElementById('month').value,
expiration_year: document.getElementById('year').value,
security_code: document.getElementById('cvv').value
};
// Create token
const token = await Omise.createToken('card', cardData);
// Send token to backend
const response = await fetch('/api/save-card', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: token.id,
customerId: 'cust_test_123456'
})
});
return response.json();
}
// Backend: Save card to customer
app.post('/api/save-card', async (req, res) => {
try {
const { token, customerId } = req.body;
const customer = await omise.customers.update(customerId, {
card: token
});
res.json({
success: true,
cardId: customer.cards.data[0].id
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
});
Managing Payment Methodsโ
List Saved Cardsโ
const customer = await omise.customers.retrieve('cust_test_123456');
customer.cards.data.forEach(card => {
console.log(`Card ID: ${card.id}`);
console.log(`Brand: ${card.brand}`);
console.log(`Last 4: ${card.last_digits}`);
console.log(`Expires: ${card.expiration_month}/${card.expiration_year}`);
console.log(`Default: ${card.id === customer.default_card}`);
console.log('---');
});
customer = omise.Customer.retrieve('cust_test_123456')
for card in customer.cards.data:
print(f'Card ID: {card.id}')
print(f'Brand: {card.brand}')
print(f'Last 4: {card.last_digits}')
print(f'Expires: {card.expiration_month}/{card.expiration_year}')
print(f'Default: {card.id == customer.default_card}')
print('---')
Set Default Cardโ
// Change default card
await omise.customers.update('cust_test_123456', {
default_card: 'card_test_789012'
});
// Verify change
const customer = await omise.customers.retrieve('cust_test_123456');
console.log('New default:', customer.default_card);
# Change default card
customer = omise.Customer.retrieve('cust_test_123456')
customer.update(default_card='card_test_789012')
# Verify change
customer.reload()
print(f'New default: {customer.default_card}')
curl https://api.omise.co/customers/cust_test_123456 \
-u skey_test_123: \
-d "default_card=card_test_789012"
Remove Cardโ
// Delete specific card
await omise.customers.destroyCard('cust_test_123456', 'card_test_789012');
// Verify removal
const customer = await omise.customers.retrieve('cust_test_123456');
console.log(`Remaining cards: ${customer.cards.total}`);
# Delete specific card
customer = omise.Customer.retrieve('cust_test_123456')
customer.destroy_card('card_test_789012')
# Verify removal
customer.reload()
print(f'Remaining cards: {customer.cards.total}')
curl https://api.omise.co/customers/cust_test_123456/cards/card_test_789012 \
-u skey_test_123: \
-X DELETE
Retrieve Specific Cardโ
// Get card details
const customer = await omise.customers.retrieve('cust_test_123456');
const card = customer.cards.data.find(c => c.id === 'card_test_789012');
console.log({
id: card.id,
brand: card.brand,
lastDigits: card.last_digits,
expiry: `${card.expiration_month}/${card.expiration_year}`,
name: card.name,
fingerprint: card.fingerprint
});
curl https://api.omise.co/customers/cust_test_123456/cards/card_test_789012 \
-u skey_test_123:
Charging Saved Payment Methodsโ
Charge Default Cardโ
// Charge customer's default card
const charge = await omise.charges.create({
customer: 'cust_test_123456',
amount: 100000, // 1,000.00 THB
currency: 'THB',
description: 'Monthly subscription - January 2024'
});
console.log('Charge status:', charge.status);
console.log('Card used:', charge.card.last_digits);
# Charge customer's default card
charge = omise.Charge.create(
customer='cust_test_123456',
amount=100000,
currency='THB',
description='Monthly subscription - January 2024'
)
print(f'Charge status: {charge.status}')
print(f'Card used: {charge.card.last_digits}')
Charge Specific Cardโ
// Charge a specific saved card
const charge = await omise.charges.create({
customer: 'cust_test_123456',
card: 'card_test_789012',
amount: 100000,
currency: 'THB',
description: 'Purchase with backup card'
});
# Charge a specific saved card
charge = omise.Charge.create(
customer='cust_test_123456',
card='card_test_789012',
amount=100000,
currency='THB',
description='Purchase with backup card'
)
CVV-less Chargingโ
// Saved cards don't require CVV for subsequent charges
const charge = await omise.charges.create({
customer: 'cust_test_123456',
amount: 50000,
currency: 'THB',
description: 'Auto-renewal payment'
});
// First charge may require 3D Secure
if (charge.authorize_uri) {
console.log('3DS required:', charge.authorize_uri);
// Redirect user for authentication
}
// Subsequent charges typically don't require 3DS
console.log('Charge successful:', charge.status === 'successful');
Charging with Fallbackโ
async function chargeWithFallback(customerId, amount, currency) {
const customer = await omise.customers.retrieve(customerId);
const cards = customer.cards.data;
// Try primary card first
try {
return await omise.charges.create({
customer: customerId,
card: customer.default_card,
amount,
currency,
description: 'Payment attempt - primary card'
});
} catch (primaryError) {
console.log('Primary card failed:', primaryError.message);
// Try backup cards
for (const card of cards) {
if (card.id === customer.default_card) continue;
try {
return await omise.charges.create({
customer: customerId,
card: card.id,
amount,
currency,
description: 'Payment attempt - backup card'
});
} catch (backupError) {
console.log(`Backup card ${card.last_digits} failed`);
}
}
throw new Error('All payment methods failed');
}
}
Card Lifecycle Managementโ
Monitor Expiring Cardsโ
async function getExpiringCards(daysAhead = 30) {
const customers = await omise.customers.list({ limit: 100 });
const expiringCards = [];
const today = new Date();
const futureDate = new Date();
futureDate.setDate(today.getDate() + daysAhead);
for (const customer of customers.data) {
for (const card of customer.cards.data) {
const cardExpiry = new Date(
card.expiration_year,
card.expiration_month - 1,
1
);
if (cardExpiry <= futureDate && cardExpiry >= today) {
expiringCards.push({
customer: customer.id,
email: customer.email,
card: card.id,
lastDigits: card.last_digits,
brand: card.brand,
expiryMonth: card.expiration_month,
expiryYear: card.expiration_year
});
}
}
}
return expiringCards;
}
// Send notifications
const expiringCards = await getExpiringCards(30);
expiringCards.forEach(card => {
console.log(
`Notify ${card.email}: ${card.brand} โขโขโขโข ${card.lastDigits} ` +
`expires ${card.expiryMonth}/${card.expiryYear}`
);
// Send email notification
});
Update Expired Cardsโ
class CardUpdateService {
async requestCardUpdate(customerId, cardId) {
// Generate secure update link
const updateToken = this.generateUpdateToken(customerId, cardId);
const updateUrl = `https://yourapp.com/update-card?token=${updateToken}`;
// Send email to customer
await this.sendUpdateEmail(customerId, updateUrl);
return updateUrl;
}
async processCardUpdate(customerId, newCardToken) {
// Get old card details
const customer = await omise.customers.retrieve(customerId);
const oldCard = customer.cards.data[0];
// Add new card
await omise.customers.update(customerId, {
card: newCardToken
});
// Get updated customer with new card
const updatedCustomer = await omise.customers.retrieve(customerId);
const newCard = updatedCustomer.cards.data[0];
// Set as default
await omise.customers.update(customerId, {
default_card: newCard.id
});
// Remove old card
await omise.customers.destroyCard(customerId, oldCard.id);
return newCard;
}
}
Handle Failed Paymentsโ
async function handleFailedPayment(charge, customerId) {
const failureCode = charge.failure_code;
const failureMessage = charge.failure_message;
console.log(`Payment failed: ${failureCode} - ${failureMessage}`);
// Handle specific failure types
switch (failureCode) {
case 'insufficient_funds':
// Retry in a few days
await this.scheduleRetry(customerId, charge.amount, 3);
await this.notifyCustomer(customerId, 'insufficient_funds');
break;
case 'expired_card':
case 'invalid_card':
// Request card update
await this.requestCardUpdate(customerId);
await this.notifyCustomer(customerId, 'card_update_required');
break;
case 'stolen_or_lost_card':
// Immediately require new card
await this.suspendSubscription(customerId);
await this.notifyCustomer(customerId, 'security_issue');
break;
default:
// Try backup card if available
const customer = await omise.customers.retrieve(customerId);
if (customer.cards.total > 1) {
await this.tryBackupCard(customerId, charge.amount);
}
}
}
Advanced Card Managementโ
Multi-Card Supportโ
class MultiCardManager {
async addCard(customerId, cardToken, metadata = {}) {
const customer = await omise.customers.retrieve(customerId);
// Check if card already exists (by fingerprint)
const existingCard = await this.findDuplicateCard(
customer,
cardToken
);
if (existingCard) {
return { duplicate: true, card: existingCard };
}
// Add new card
await omise.customers.update(customerId, {
card: cardToken
});
const updatedCustomer = await omise.customers.retrieve(customerId);
const newCard = updatedCustomer.cards.data[0];
// Store card metadata in your database
await this.saveCardMetadata(customerId, newCard.id, metadata);
return { duplicate: false, card: newCard };
}
async findDuplicateCard(customer, cardToken) {
// Get card fingerprint from token
const token = await omise.tokens.retrieve(cardToken);
const fingerprint = token.card.fingerprint;
// Check existing cards
return customer.cards.data.find(
card => card.fingerprint === fingerprint
);
}
async organizeCards(customerId) {
const customer = await omise.customers.retrieve(customerId);
const cards = customer.cards.data;
// Separate by validity
const valid = [];
const expiring = [];
const expired = [];
const today = new Date();
const in30Days = new Date();
in30Days.setDate(today.getDate() + 30);
cards.forEach(card => {
const expiry = new Date(
card.expiration_year,
card.expiration_month - 1,
1
);
if (expiry < today) {
expired.push(card);
} else if (expiry < in30Days) {
expiring.push(card);
} else {
valid.push(card);
}
});
return { valid, expiring, expired };
}
}
Card Validation Rulesโ
class CardValidator {
async validateCard(cardToken) {
const token = await omise.tokens.retrieve(cardToken);
const card = token.card;
// Check expiry
const expiry = new Date(
card.expiration_year,
card.expiration_month - 1,
1
);
if (expiry <= new Date()) {
return { valid: false, reason: 'Card has expired' };
}
// Check security code
if (!card.security_code_check) {
return { valid: false, reason: 'Security code check failed' };
}
// Check brand
const allowedBrands = ['Visa', 'MasterCard', 'JCB'];
if (!allowedBrands.includes(card.brand)) {
return { valid: false, reason: 'Card brand not supported' };
}
return { valid: true };
}
async testCard(customerId, cardToken) {
// Create small test charge
try {
const charge = await omise.charges.create({
customer: customerId,
card: cardToken,
amount: 100, // 1.00 THB
currency: 'THB',
description: 'Card verification charge'
});
// Refund immediately
if (charge.status === 'successful') {
await omise.refunds.create(charge.id, {
amount: charge.amount
});
}
return { valid: true };
} catch (error) {
return { valid: false, reason: error.message };
}
}
}
Smart Retry Logicโ
class PaymentRetryService {
async retryFailedPayment(customerId, amount, currency, attempt = 1) {
const maxAttempts = 3;
const backoffDays = [1, 3, 7]; // Retry after 1, 3, and 7 days
if (attempt > maxAttempts) {
await this.handleMaxRetriesReached(customerId);
return null;
}
try {
const charge = await omise.charges.create({
customer: customerId,
amount,
currency,
description: `Retry attempt ${attempt} of ${maxAttempts}`
});
if (charge.status === 'successful') {
await this.notifyRetrySuccess(customerId, attempt);
return charge;
}
} catch (error) {
console.log(`Attempt ${attempt} failed:`, error.message);
// Schedule next retry
if (attempt < maxAttempts) {
const nextRetryDays = backoffDays[attempt];
await this.scheduleRetry(
customerId,
amount,
currency,
nextRetryDays,
attempt + 1
);
}
}
return null;
}
async scheduleRetry(customerId, amount, currency, days, attempt) {
const retryDate = new Date();
retryDate.setDate(retryDate.getDate() + days);
// Store retry info in your database
await this.saveRetrySchedule({
customerId,
amount,
currency,
retryDate,
attempt
});
// Notify customer
await this.notifyRetryScheduled(customerId, retryDate, attempt);
}
}
Common Use Casesโ
Subscription Billingโ
class SubscriptionBilling {
async processMonthlyBilling() {
const subscriptions = await this.getActiveSubscriptions();
for (const sub of subscriptions) {
try {
const charge = await omise.charges.create({
customer: sub.customerId,
amount: sub.amount,
currency: 'THB',
description: `${sub.planName} - ${this.getCurrentBillingPeriod()}`,
metadata: {
subscription_id: sub.id,
billing_period: this.getCurrentBillingPeriod()
}
});
if (charge.status === 'successful') {
await this.recordSuccessfulPayment(sub.id, charge.id);
await this.notifyCustomer(sub.customerId, 'payment_success');
}
} catch (error) {
await this.handleFailedPayment(sub, error);
}
}
}
getCurrentBillingPeriod() {
const now = new Date();
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
}
}
Usage-Based Billingโ
class UsageBasedBilling {
async chargeForUsage(customerId, usageData) {
// Calculate amount based on usage
const amount = this.calculateAmount(usageData);
if (amount === 0) {
console.log('No usage to charge');
return null;
}
const charge = await omise.charges.create({
customer: customerId,
amount,
currency: 'THB',
description: `Usage billing: ${usageData.units} units`,
metadata: {
usage_period: usageData.period,
units_used: usageData.units,
rate: usageData.rate
}
});
return charge;
}
calculateAmount(usageData) {
const { units, rate, includedUnits } = usageData;
const billableUnits = Math.max(0, units - includedUnits);
return billableUnits * rate;
}
}
Wallet Managementโ
class WalletService {
async topUpWallet(customerId, amount) {
// Charge saved card
const charge = await omise.charges.create({
customer: customerId,
amount,
currency: 'THB',
description: 'Wallet top-up'
});
if (charge.status === 'successful') {
// Add to wallet balance in your database
await this.addToWalletBalance(customerId, amount);
}
return charge;
}
async autoTopUp(customerId) {
const balance = await this.getWalletBalance(customerId);
const threshold = 10000; // 100.00 THB
const topUpAmount = 50000; // 500.00 THB
if (balance < threshold) {
return await this.topUpWallet(customerId, topUpAmount);
}
return null;
}
}
Best Practicesโ
Securityโ
// 1. Never store raw card data
// โ
Good - Use tokens
const customer = await omise.customers.create({
email: 'user@example.com',
card: tokenFromClient
});
// โ Bad - Don't store card numbers
// NEVER DO THIS
const user = {
cardNumber: '4242424242424242',
cvv: '123'
};
// 2. Validate before saving
async function saveCardSecurely(customerId, cardToken) {
const validation = await validateCard(cardToken);
if (!validation.valid) {
throw new Error(`Invalid card: ${validation.reason}`);
}
return await omise.customers.update(customerId, {
card: cardToken
});
}
// 3. Monitor for suspicious activity
async function detectFraud(customerId, charge) {
const recentCharges = await this.getRecentCharges(customerId);
// Multiple failed attempts
const failedAttempts = recentCharges.filter(
c => c.status === 'failed'
).length;
if (failedAttempts >= 3) {
await this.flagForReview(customerId);
throw new Error('Multiple failed payment attempts detected');
}
}
User Experienceโ
// 1. Show clear card information
function displaySavedCard(card) {
return {
display: `${card.brand} โขโขโขโข ${card.last_digits}`,
expiry: `${card.expiration_month}/${card.expiration_year}`,
isDefault: card.id === customer.default_card,
isExpiring: isCardExpiring(card),
icon: getCardBrandIcon(card.brand)
};
}
// 2. Proactive expiry notifications
async function sendExpiryReminders() {
const expiringIn30Days = await getExpiringCards(30);
const expiringIn7Days = await getExpiringCards(7);
// First reminder - 30 days
for (const card of expiringIn30Days) {
await sendEmail(card.email, 'card-expiring-30-days', card);
}
// Final reminder - 7 days
for (const card of expiringIn7Days) {
await sendEmail(card.email, 'card-expiring-7-days', card);
}
}
// 3. Graceful failure handling
async function chargeWithGracefulFailure(customerId, amount) {
try {
return await omise.charges.create({
customer: customerId,
amount,
currency: 'THB'
});
} catch (error) {
// User-friendly error messages
const userMessage = this.getUserFriendlyMessage(error);
// Log for debugging
console.error('Charge failed:', error);
// Notify customer
await this.notifyCustomer(customerId, userMessage);
throw new Error(userMessage);
}
}
Performanceโ
// 1. Cache customer data
class CustomerCache {
constructor() {
this.cache = new Map();
this.ttl = 5 * 60 * 1000; // 5 minutes
}
async getCustomer(customerId) {
const cached = this.cache.get(customerId);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}
const customer = await omise.customers.retrieve(customerId);
this.cache.set(customerId, {
data: customer,
timestamp: Date.now()
});
return customer;
}
}
// 2. Batch operations
async function batchChargeCustomers(charges) {
const results = await Promise.allSettled(
charges.map(({ customerId, amount }) =>
omise.charges.create({
customer: customerId,
amount,
currency: 'THB'
})
)
);
const successful = results.filter(r => r.status === 'fulfilled');
const failed = results.filter(r => r.status === 'rejected');
return { successful, failed };
}
Troubleshootingโ
Card Already Existsโ
// Check for duplicate cards before adding
async function addCardSafely(customerId, cardToken) {
const token = await omise.tokens.retrieve(cardToken);
const customer = await omise.customers.retrieve(customerId);
// Check if card with same fingerprint exists
const duplicate = customer.cards.data.find(
card => card.fingerprint === token.card.fingerprint
);
if (duplicate) {
return {
added: false,
message: 'This card is already saved',
existingCard: duplicate
};
}
await omise.customers.update(customerId, { card: cardToken });
return { added: true };
}
Charge Fails with Saved Cardโ
async function diagnoseChargeFailure(customerId, error) {
console.log('Diagnosing charge failure...');
// Check customer exists
let customer;
try {
customer = await omise.customers.retrieve(customerId);
} catch (e) {
return 'Customer not found';
}
// Check has payment method
if (!customer.default_card) {
return 'No default payment method';
}
// Check card validity
const card = customer.cards.data.find(
c => c.id === customer.default_card
);
const expiry = new Date(
card.expiration_year,
card.expiration_month - 1,
1
);
if (expiry < new Date()) {
return 'Card has expired';
}
// Check error code
if (error.code === 'insufficient_funds') {
return 'Insufficient funds on card';
}
return 'Unknown error - contact support';
}
Cannot Remove Cardโ
async function removeCardSafely(customerId, cardId) {
const customer = await omise.customers.retrieve(customerId);
// Prevent removing only card
if (customer.cards.total === 1) {
throw new Error('Cannot remove the only payment method');
}
// If removing default, set new default first
if (customer.default_card === cardId) {
const otherCard = customer.cards.data.find(c => c.id !== cardId);
await omise.customers.update(customerId, {
default_card: otherCard.id
});
}
// Now safe to remove
await omise.customers.destroyCard(customerId, cardId);
}
FAQโ
General Questionsโ
Q: How long are saved cards valid?
A: Saved cards remain valid until they expire or are manually removed. Monitor expiration dates and request updates proactively.
Q: Can customers save cards from different countries?
A: Yes, customers can save cards from any country, subject to your account's supported countries and currencies.
Q: Do saved cards require CVV for charges?
A: No, once a card is saved (with CVV verified initially), subsequent charges don't require CVV.
Q: Can I migrate saved cards between systems?
A: No, for security reasons, card tokens cannot be exported. Customers must re-enter payment information when migrating platforms.
Q: What happens if a saved card expires?
A: Charges will fail. Proactively monitor expiration dates and request updated card information from customers.
Q: How many cards can a customer save?
A: There's no strict limit, but for best UX, we recommend allowing 3-5 active cards per customer.
Security Questionsโ
Q: Are saved cards PCI compliant?
A: Yes, Omise handles all card storage in PCI DSS Level 1 compliant infrastructure. You don't store any card data.
Q: Can saved cards be used fraudulently?
A: Omise implements fraud detection and 3D Secure when appropriate. Monitor for suspicious patterns in your application.
Q: Should I implement additional verification for saved card charges?
A: For high-value transactions or suspicious activity, consider requiring additional authentication (SMS OTP, email confirmation, etc.).
Q: How do I handle compromised cards?
A: Remove the compromised card immediately and notify the customer to add a new payment method.
Technical Questionsโ
Q: Can I charge a saved card in a different currency?
A: Yes, you can charge saved cards in any supported currency, subject to your account settings.
Q: What's the difference between card and token parameters?
A: Use card (token ID) when saving a new card. Use card (card ID) when charging a specific saved card.
Q: Can I update saved card details?
A: No, you cannot update card details. Remove the old card and add a new one with updated information.
Q: How do I handle 3D Secure with saved cards?
A: First charge may require 3D Secure. Subsequent charges typically don't, but banks may request it for high-risk transactions.
Related Resourcesโ
- Customer Management
- Recurring Schedules
- Tokens API Reference
- Cards API Reference
- Charges API Reference
- Security Best Practices
- Webhooks