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

保存された支払い方法

継続課金、ワンクリックチェックアウト体験、サブスクリプションサービスのために支払い方法を安全に保存および管理する方法を学びます。このガイドでは、カードの保存、管理、および保存された支払い方法での課金について説明します。

概要

保存された支払い方法により、取引ごとに支払い詳細を収集することなく顧客に課金できます。これは以下の場合に不可欠です:

  • 継続的なサブスクリプション課金
  • ワンクリックチェックアウト体験
  • 自動更新サービス
  • メンバーシッププラットフォーム
  • 使用量ベースの課金

主な利点

  • コンバージョンの向上 - ワンクリック決済で摩擦を減らす
  • リテンション向上 - シームレスなサブスクリプション更新
  • PCI範囲の削減 - 生のカードデータではなくトークンを使用
  • 複数の支払いオプション - 複数の保存されたカードをサポート
  • セキュリティ強化 - 初回使用時に3Dセキュア、その後の課金は高速

セキュリティとコンプライアンス

Omiseはすべてのカード保存を安全に処理します:

  • カードはトークン化および暗号化されます
  • PCI DSS レベル1準拠のインフラストラクチャ
  • 生のカードデータがサーバーに触れることはありません
  • カード保存用のセキュアボルト
  • データ保護規制への準拠

支払い方法の保存

顧客作成時にカードを保存

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 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"

クライアントサイドのカード保存フロー

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

支払い方法の管理

保存されたカードのリスト表示

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('---')

デフォルトカードの設定

// 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"

カードの削除

// 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

特定のカードの取得

// 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:

保存された支払い方法での課金

デフォルトカードへの課金

// 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 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なしでの課金

// 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');

フォールバック機能付き課金

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

カードライフサイクル管理

期限切れ間近のカードの監視

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

期限切れカードの更新

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

失敗した支払いの処理

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

ベストプラクティス

セキュリティ

// 1. 生のカードデータを保存しない
// ✅ 良い - トークンを使用
const customer = await omise.customers.create({
email: 'user@example.com',
card: tokenFromClient
});

// ❌ 悪い - カード番号を保存しない
// 絶対にこれをしないでください
const user = {
cardNumber: '4242424242424242',
cvv: '123'
};

// 2. 保存前に検証
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. 不審なアクティビティの監視
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');
}
}

ユーザーエクスペリエンス

// 1. カード情報を明確に表示
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. 積極的な有効期限通知
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. 適切な失敗処理
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);
}
}

よくある質問

一般的な質問

Q: 保存されたカードはどのくらい有効ですか?

A: 保存されたカードは、有効期限が切れるか手動で削除されるまで有効です。有効期限を監視し、積極的に更新を要求してください。

Q: 顧客は異なる国のカードを保存できますか?

A: はい、顧客はどの国のカードも保存できます。ただし、アカウントでサポートされている国と通貨に従います。

Q: 保存されたカードの課金にはCVVが必要ですか?

A: いいえ、カードが保存されると(最初にCVVが検証されます)、その後の課金にはCVVは必要ありません。

Q: 保存されたカードをシステム間で移行できますか?

A: いいえ、セキュリティ上の理由から、カードトークンはエクスポートできません。プラットフォームを移行する際、顧客は支払い情報を再入力する必要があります。

Q: 保存されたカードの有効期限が切れたらどうなりますか?

A: 課金は失敗します。有効期限を積極的に監視し、顧客に更新されたカード情報を要求してください。

Q: 顧客は何枚のカードを保存できますか?

A: 厳密な制限はありませんが、最高のUXのために、顧客あたり3〜5枚のアクティブなカードを許可することをお勧めします。

セキュリティに関する質問

Q: 保存されたカードはPCI準拠ですか?

A: はい、OmiseはPCI DSS レベル1準拠のインフラストラクチャですべてのカード保存を処理します。カードデータは保存されません。

Q: 保存されたカードは不正使用される可能性がありますか?

A: Omiseは不正検出と適切な場合の3Dセキュアを実装しています。アプリケーションで不審なパターンを監視してください。

Q: 保存されたカードの課金に追加の検証を実装すべきですか?

A: 高額取引や不審なアクティビティの場合、追加の認証(SMS OTP、メール確認など)を要求することを検討してください。

Q: 侵害されたカードはどう処理しますか?

A: 侵害されたカードを直ちに削除し、顧客に新しい支払い方法を追加するよう通知してください。

関連リソース

次のステップ