Skip to main content

Saved Cards & Recurring Payments

Store customer cards securely for future charges using the Customers API. Perfect for subscriptions, recurring billing, and returning customers.

Overviewโ€‹

The Omise Customers API enables you to save payment methods for future use. Instead of asking customers to re-enter card details for every purchase, you can securely store their cards and charge them with a single API call.

Use Cases:

  • Monthly subscriptions (Netflix, Spotify model)
  • Recurring billing (utilities, rent)
  • One-click checkout for returning customers
  • Automatic renewals
  • Installment payments

How It Worksโ€‹

Implementation Guideโ€‹

Step 1: Create Customer with Cardโ€‹

When a customer makes their first purchase, create a Customer object:

curl https://api.omise.co/customers \
-u skey_test_YOUR_SECRET_KEY: \
-d "email=john@example.com" \
-d "description=John Doe - Premium Member" \
-d "card=tokn_test_5rt6s9vah5lkvi1rh9c"

Response:

{
"object": "customer",
"id": "cust_test_5rt6s9vah5lkvi1rh9c",
"email": "john@example.com",
"description": "John Doe - Premium Member",
"default_card": "card_test_5rt6s9vah5lkvi1rh9c",
"cards": {
"object": "list",
"data": [
{
"object": "card",
"id": "card_test_5rt6s9vah5lkvi1rh9c",
"brand": "Visa",
"last_digits": "4242",
"expiration_month": 12,
"expiration_year": 2027
}
]
},
"created_at": "2024-01-15T10:30:00Z"
}

Step 2: Store Customer IDโ€‹

Save the customer ID in your database alongside user information:

-- Example database schema
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255),
omise_customer_id VARCHAR(50), -- Store this!
subscription_status VARCHAR(20),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Insert user with customer ID
INSERT INTO users (email, name, omise_customer_id, subscription_status)
VALUES ('john@example.com', 'John Doe', 'cust_test_...', 'active');

Step 3: Charge Saved Cardโ€‹

For future payments, charge using the customer ID instead of a token:

curl https://api.omise.co/charges \
-u skey_test_YOUR_SECRET_KEY: \
-d "amount=29900" \
-d "currency=thb" \
-d "customer=cust_test_5rt6s9vah5lkvi1rh9c" \
-d "description=Monthly Subscription - February 2024"
No Token Needed

When charging a customer ID, you don't need to create a token. Omise automatically uses the customer's default card.

Managing Multiple Cardsโ€‹

Add Additional Cardโ€‹

// Create token for new card via Omise.js first
const token = 'tokn_test_...';

// Add card to existing customer
const card = await omise.customers.addCard(customerId, {
card: token
});

List Customer's Cardsโ€‹

curl https://api.omise.co/customers/cust_test_.../cards \
-u skey_test_YOUR_SECRET_KEY:

Set Default Cardโ€‹

await omise.customers.update(customerId, {
default_card: 'card_test_...'
});

Delete Cardโ€‹

curl https://api.omise.co/customers/cust_test_.../cards/card_test_... \
-X DELETE \
-u skey_test_YOUR_SECRET_KEY:

Recurring Payments Implementationโ€‹

Example: Monthly Subscriptionโ€‹

// Subscription billing function (run daily via cron)
async function processSubscriptionBilling() {
// Find subscriptions due for renewal today
const dueSubscriptions = await db.query(`
SELECT user_id, omise_customer_id, subscription_plan, amount
FROM users
WHERE subscription_status = 'active'
AND next_billing_date = CURRENT_DATE
`);

for (const sub of dueSubscriptions) {
try {
// Charge the customer
const charge = await omise.charges.create({
amount: sub.amount,
currency: 'thb',
customer: sub.omise_customer_id,
description: `${sub.subscription_plan} - ${new Date().toISOString().slice(0, 7)}`,
metadata: {
user_id: sub.user_id,
subscription_type: sub.subscription_plan
}
});

if (charge.status === 'successful') {
// Update next billing date
await db.query(`
UPDATE users
SET next_billing_date = DATE_ADD(CURRENT_DATE, INTERVAL 1 MONTH),
last_charge_id = ?
WHERE user_id = ?
`, [charge.id, sub.user_id]);

// Send receipt email
await sendReceiptEmail(sub.user_id, charge);

} else {
// Handle failed payment
await handleFailedPayment(sub.user_id, charge.failure_message);
}

} catch (error) {
console.error(`Failed to charge user ${sub.user_id}:`, error);
await handleBillingError(sub.user_id, error);
}
}
}

Retry Logic for Failed Paymentsโ€‹

async function handleFailedPayment(userId, failureMessage) {
const user = await db.getUserById(userId);
const retryCount = user.payment_retry_count || 0;

if (retryCount < 3) {
// Schedule retry
await db.query(`
UPDATE users
SET payment_retry_count = ?,
next_retry_date = DATE_ADD(CURRENT_DATE, INTERVAL ? DAY)
WHERE user_id = ?
`, [retryCount + 1, retryCount + 1, userId]);

// Notify customer
await sendPaymentFailedEmail(userId, {
reason: failureMessage,
retryDate: new Date(Date.now() + (retryCount + 1) * 24 * 60 * 60 * 1000)
});

} else {
// Max retries reached - suspend subscription
await db.query(`
UPDATE users
SET subscription_status = 'suspended',
suspension_reason = 'payment_failure'
WHERE user_id = ?
`, [userId]);

await sendSubscriptionSuspendedEmail(userId);
}
}

Update Card Informationโ€‹

Update Expiration Dateโ€‹

Customers can update card expiration without re-entering card number:

await omise.customers.updateCard(customerId, cardId, {
expiration_month: 12,
expiration_year: 2028,
name: 'John Doe',
postal_code: '10110'
});

Replace Card Entirelyโ€‹

For security reasons, card numbers cannot be updated. To replace a card:

  1. Create new token with Omise.js
  2. Add new card to customer
  3. Set as default card
  4. Delete old card
// 1. Create token via Omise.js (client-side)
const newToken = 'tokn_test_...';

// 2. Add new card
const newCard = await omise.customers.addCard(customerId, {
card: newToken
});

// 3. Set as default
await omise.customers.update(customerId, {
default_card: newCard.id
});

// 4. Delete old card
await omise.customers.destroyCard(customerId, oldCardId);

Security & Complianceโ€‹

PCI Complianceโ€‹

Omise handles card storage in PCI-compliant vaults. You only store:

  • Customer ID (safe to store)
  • Card metadata (last 4 digits, brand, expiration - safe to display)

Never store:

  • Full card number
  • CVV/security code
  • Raw card data

Customer Data Protectionโ€‹

// โœ… GOOD: Store only IDs and metadata
const user = {
id: 12345,
email: 'john@example.com',
omise_customer_id: 'cust_test_...',
card_last_digits: '4242', // Safe to display
card_brand: 'Visa', // Safe to display
card_expiry: '12/2027' // Safe to display
};

// โŒ BAD: Never store these
const badExample = {
card_number: '4242424242424242', // DON'T STORE THIS!
cvv: '123' // DON'T STORE THIS!
};

Always obtain explicit consent before saving cards:

<form id="payment-form">
<!-- Card input fields -->

<label>
<input type="checkbox" id="save-card" name="save_card" />
Save this card for future purchases
</label>

<button type="submit">Complete Payment</button>
</form>

<script>
document.getElementById('payment-form').addEventListener('submit', function(e) {
e.preventDefault();
const saveCard = document.getElementById('save-card').checked;

// If saving card, create customer
// If not, just create charge with token
});
</script>

Common Issues & Troubleshootingโ€‹

Issue: Customer Already Existsโ€‹

Error: customer_already_exists

Solution: Retrieve existing customer or update it:

try {
const customer = await omise.customers.create({
email: email,
card: token
});
} catch (error) {
if (error.code === 'customer_already_exists') {
// Retrieve and update instead
const existingCustomer = await omise.customers.list({
limit: 1,
// Use your database to find customer ID
});
}
}

Issue: Default Card Missingโ€‹

Error: default_card_not_found

Cause: Customer has no cards or default card was deleted

Solution:

// Check if customer has cards
const customer = await omise.customers.retrieve(customerId);

if (customer.cards.total === 0) {
// Prompt user to add card
throw new Error('Please add a payment method');
}

if (!customer.default_card) {
// Set first card as default
await omise.customers.update(customerId, {
default_card: customer.cards.data[0].id
});
}

Issue: Card Expiredโ€‹

Solution: Implement expiration monitoring:

// Run monthly to notify customers of expiring cards
async function notifyExpiringCards() {
const nextMonth = new Date();
nextMonth.setMonth(nextMonth.getMonth() + 1);

const month = nextMonth.getMonth() + 1;
const year = nextMonth.getFullYear();

// Find users with cards expiring next month
const users = await db.query(`
SELECT u.email, c.expiration_month, c.expiration_year
FROM users u
JOIN cards c ON u.omise_customer_id = c.customer_id
WHERE c.expiration_month = ? AND c.expiration_year = ?
`, [month, year]);

for (const user of users) {
await sendCardExpiringEmail(user.email, {
expiryMonth: month,
expiryYear: year
});
}
}

Best Practicesโ€‹

  1. Always Handle Payment Failures Gracefully

    • Implement retry logic
    • Notify customers promptly
    • Provide easy card update flow
  2. Monitor Card Expirations

    • Send reminders 30 days before expiration
    • Provide one-click card update
    • Pause subscriptions if card update fails
  3. Provide Subscription Management

    • Allow customers to view saved cards
    • Easy card deletion
    • Subscription cancellation options
    • Billing history access
  4. Use Metadata Effectively

    await omise.customers.create({
    email: email,
    card: token,
    metadata: {
    user_id: '12345',
    signup_source: 'mobile_app',
    subscription_tier: 'premium',
    referral_code: 'FRIEND2024'
    }
    });
  5. Implement Webhooks Listen for charge.complete and charge.failed events:

    // In your webhook handler
    if (event.key === 'charge.complete') {
    await updateSubscriptionStatus(charge.customer, 'active');
    } else if (event.key === 'charge.failed') {
    await handleFailedPayment(charge.customer, charge.failure_message);
    }

FAQโ€‹

Can customers have multiple cards?

Yes! Customers can have multiple cards attached. Use the default_card field to specify which card to charge automatically. Customers can switch their default card at any time.

How do I charge a specific card instead of the default?

Specify the card parameter when creating a charge:

await omise.charges.create({
amount: 10000,
currency: 'thb',
customer: customerId,
card: 'card_test_specific_card_id'
});
Can I use saved cards with 3D Secure?

The first charge typically requires 3D Secure authentication. Subsequent charges may not require authentication, depending on the issuing bank. This is called "frictionless flow" and is handled automatically by banks based on risk assessment.

What happens if a customer deletes their account?

You should also delete the Customer object from Omise:

await omise.customers.destroy(customerId);

This removes all associated cards and complies with data deletion requirements (GDPR, etc.).

Can I migrate existing customers to Omise?

If you're migrating from another payment processor, customers will need to re-enter their card details. You cannot transfer card data between payment processors for security reasons.

Create a migration flow:

  1. Notify customers of the change
  2. Provide incentive to update payment method
  3. Implement deadline with grace period
  4. Suspend subscriptions for non-compliance
How do I test saved cards?

Use test mode with test cards:

  1. Create customer with test token
  2. Make test charges
  3. Test card updates and deletions
  4. Test failure scenarios

All test data is isolated from live mode.

View testing guide โ†’

Next Stepsโ€‹

  1. Create your first customer
  2. Implement recurring billing
  3. Set up failure handling
  4. Configure webhooks
  5. Test thoroughly
  6. Go live