カスタム統合
Omise.jsの低レベルAPIを使用して、デザインとユーザーエクスペリエンスを完全にコントロールした独自の決済UIを構築します。
概要
カスタム統合では、Omise.jsが安全なトークン化を処理しながら、決済UIを完全にコントロールできます。以下に最適です:
- カスタムブランディング - サイトのデザインに完璧にマッチ
- 複雑なチェックアウトフロー - マルチステップチェックアウト、ワンページチェックアウト
- A/Bテスト - 異なるUIバリエーションをテスト
- フレームワーク統合 - React、Vue、Angularコンポーネント
- モバイルアプリ - WebView統合
トレードオフ:
- ✅ 完全なデザインコントロール
- ✅ カスタムバリデーションロジック
- ✅ フレームワークフレンドリー
- ❌ より多くの開発時間
- ❌ UI/UXを自分で処理
- ❌ エラーメッセージを自分で処理
カスタム決済フォームの例
カスタム設計されたクレジットカード決済フォームの例:

コアコンセプト
1. トークン化フロー
2. カードデータを絶対にサーバーに送信しない
// ❌ 間違い - これをしないでください!
fetch('/checkout', {
body: JSON.stringify({
cardNumber: '4242424242424242',
cvv: '123',
expiry: '12/25'
})
});
// ✅ 正しい - トークンのみを送信
Omise.createToken('card', cardData, (statusCode, response) => {
if (statusCode === 200) {
fetch('/checkout', {
body: JSON.stringify({
token: response.id // トークンのみ を送信!
})
});
}
});
基本的な実装
HTMLフォーム
<!DOCTYPE html>
<html>
<head>
<title>Custom Checkout</title>
<style>
.payment-form {
max-width: 400px;
margin: 50px auto;
padding: 30px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-family: system-ui, -apple-system, sans-serif;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
.form-group input {
width: 100%;
padding: 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px;
box-sizing: border-box;
}
.form-group input:focus {
outline: none;
border-color: #1e3a8a;
box-shadow: 0 0 0 3px rgba(28, 126, 214, 0.1);
}
.form-group input.error {
border-color: #c92a2a;
}
.error-message {
color: #c92a2a;
font-size: 14px;
margin-top: 5px;
}
.form-row {
display: flex;
gap: 15px;
}
.form-row .form-group {
flex: 1;
}
.btn-pay {
width: 100%;
padding: 14px;
background: #1e3a8a;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.btn-pay:hover {
background: #1864ab;
}
.btn-pay:disabled {
background: #adb5bd;
cursor: not-allowed;
}
.card-icons {
display: flex;
gap: 8px;
margin-top: 8px;
}
.card-icons img {
height: 24px;
}
</style>
</head>
<body>
<div class="payment-form">
<h2>Payment Details</h2>
<p style="color: #666; margin-bottom: 30px;">Amount: <strong>฿100.25</strong></p>
<form id="payment-form">
<div class="form-group">
<label for="card-name">Cardholder Name</label>
<input
type="text"
id="card-name"
name="card-name"
placeholder="John Doe"
autocomplete="cc-name"
required
/>
<div class="error-message" id="name-error"></div>
</div>
<div class="form-group">
<label for="card-number">Card Number</label>
<input
type="text"
id="card-number"
name="card-number"
placeholder="4242 4242 4242 4242"
autocomplete="cc-number"
maxlength="19"
required
/>
<div class="card-icons">
<img src="/images/visa.svg" alt="Visa" />
<img src="/images/mastercard.svg" alt="Mastercard" />
<img src="/images/jcb.svg" alt="JCB" />
</div>
<div class="error-message" id="number-error"></div>
</div>
<div class="form-row">
<div class="form-group">
<label for="expiry-month">Expiry Date</label>
<div style="display: flex; gap: 10px;">
<input
type="text"
id="expiry-month"
name="expiry-month"
placeholder="MM"
autocomplete="cc-exp-month"
maxlength="2"
style="width: 60px;"
required
/>
<span style="align-self: center;">/</span>
<input
type="text"
id="expiry-year"
name="expiry-year"
placeholder="YYYY"
autocomplete="cc-exp-year"
maxlength="4"
style="width: 80px;"
required
/>
</div>
<div class="error-message" id="expiry-error"></div>
</div>
<div class="form-group">
<label for="cvv">CVV</label>
<input
type="text"
id="cvv"
name="cvv"
placeholder="123"
autocomplete="cc-csc"
maxlength="4"
required
/>
<div class="error-message" id="cvv-error"></div>
</div>
</div>
<div class="form-group" style="margin-bottom: 0;">
<button type="submit" class="btn-pay" id="pay-button">
Pay ฿100.25
</button>
</div>
</form>
</div>
<script src="https://cdn.omise.co/omise.js"></script>
<script src="/js/payment.js"></script>
</body>
</html>
JavaScript実装
// payment.js
// Omise.jsを初期化
Omise.setPublicKey("pkey_test_YOUR_PUBLIC_KEY");
const form = document.getElementById('payment-form');
const payButton = document.getElementById('pay-button');
// Card number formatting
document.getElementById('card-number').addEventListener('input', (e) => {
let value = e.target.value.replace(/\s/g, '');
let formatted = value.match(/.{1,4}/g)?.join(' ') || value;
e.target.value = formatted;
});
// Month input validation
document.getElementById('expiry-month').addEventListener('input', (e) => {
let value = e.target.value.replace(/\D/g, '');
if (value.length > 0) {
value = Math.min(parseInt(value), 12).toString().padStart(2, '0');
}
e.target.value = value;
});
// Year input validation
document.getElementById('expiry-year').addEventListener('input', (e) => {
e.target.value = e.target.value.replace(/\D/g, '');
});
// CVV input validation
document.getElementById('cvv').addEventListener('input', (e) => {
e.target.value = e.target.value.replace(/\D/g, '');
});
// Form submission
form.addEventListener('submit', async (e) => {
e.preventDefault();
// Clear previous errors
clearErrors();
// Validate form
if (!validateForm()) {
return;
}
// Disable button
payButton.disabled = true;
payButton.textContent = 'Processing...';
// Get form values
const cardData = {
name: document.getElementById('card-name').value,
number: document.getElementById('card-number').value.replace(/\s/g, ''),
expiration_month: parseInt(document.getElementById('expiry-month').value),
expiration_year: parseInt(document.getElementById('expiry-year').value),
security_code: document.getElementById('cvv').value
};
// Create token
Omise.createToken('card', cardData, async (statusCode, response) => {
if (statusCode === 200) {
// Success - send token to server
await handleTokenSuccess(response.id);
} else {
// Error - show message
handleTokenError(response);
payButton.disabled = false;
payButton.textContent = 'Pay ฿100.25';
}
});
});
function validateForm() {
let isValid = true;
// Validate name
const name = document.getElementById('card-name').value.trim();
if (!name) {
showError('name-error', 'Cardholder name is required');
isValid = false;
}
// Validate card number
const number = document.getElementById('card-number').value.replace(/\s/g, '');
if (number.length < 13 || number.length > 19) {
showError('number-error', 'Please enter a valid card number');
isValid = false;
}
// Validate expiry
const month = parseInt(document.getElementById('expiry-month').value);
const year = parseInt(document.getElementById('expiry-year').value);
if (month < 1 || month > 12) {
showError('expiry-error', 'Invalid expiry month');
isValid = false;
}
if (year < new Date().getFullYear()) {
showError('expiry-error', 'Card has expired');
isValid = false;
}
// Validate CVV
const cvv = document.getElementById('cvv').value;
if (cvv.length < 3 || cvv.length > 4) {
showError('cvv-error', 'Please enter a valid CVV');
isValid = false;
}
return isValid;
}
function showError(elementId, message) {
const errorEl = document.getElementById(elementId);
errorEl.textContent = message;
const inputId = elementId.replace('-error', '');
const inputEl = document.getElementById(inputId);
if (inputEl) {
inputEl.classList.add('error');
}
}
function clearErrors() {
document.querySelectorAll('.error-message').forEach(el => {
el.textContent = '';
});
document.querySelectorAll('input').forEach(el => {
el.classList.remove('error');
});
}
function handleTokenError(response) {
alert('Card validation failed: ' + response.message);
}
async function handleTokenSuccess(token) {
try {
const response = await fetch('/api/charge', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
token: token,
amount: 10025,
currency: 'THB'
})
});
const data = await response.json();
if (data.authorize_uri) {
// Redirect for 3D Secure
window.location.href = data.authorize_uri;
} else if (data.status === 'successful') {
// Payment successful
window.location.href = '/success?charge=' + data.id;
} else if (data.status === 'failed') {
// Payment failed
alert('Payment failed: ' + data.failure_message);
payButton.disabled = false;
payButton.textContent = 'Pay ฿100.25';
} else {
// Pending - wait for webhook
window.location.href = '/pending';
}
} catch (error) {
console.error('Error:', error);
alert('Payment error. Please try again.');
payButton.disabled = false;
payButton.textContent = 'Pay ฿100.25';
}
}
代替決済方法
ソースの作成
カード以外の決済(ウォレット、QR、バンキング)の場合は、createSourceを使用します:
// PromptPay
Omise.createSource('promptpay', {
amount: 50000,
currency: 'THB'
}, handleSourceCallback);
// TrueMoney
Omise.createSource('truemoney', {
amount: 30000,
currency: 'THB',
phone_number: '+66876543210'
}, handleSourceCallback);
// Mobile Banking
Omise.createSource('mobile_banking_kbank', {
amount: 40000,
currency: 'THB'
}, handleSourceCallback);
function handleSourceCallback(statusCode, response) {
if (statusCode === 200) {
// Send source ID to server
createChargeWithSource(response.id);
} else {
alert('Error: ' + response.message);
}
}
完全なマルチメソッドの例
複数の決済方法をサポートするカスタムフォームを作成できます:

<form id="payment-form">
<div class="payment-method-selector">
<label>
<input type="radio" name="method" value="card" checked>
Credit/Debit Card
</label>
<label>
<input type="radio" name="method" value="promptpay">
PromptPay
</label>
<label>
<input type="radio" name="method" value="truemoney">
TrueMoney Wallet
</label>
</div>
<div id="card-fields" class="payment-fields">
<!-- Card fields here -->
</div>
<div id="promptpay-fields" class="payment-fields" style="display: none;">
<p>Scan QR code to pay</p>
</div>
<div id="truemoney-fields" class="payment-fields" style="display: none;">
<label>Phone Number</label>
<input type="tel" id="truemoney-phone" placeholder="+66876543210">
</div>
<button type="submit">Pay Now</button>
</form>
<script>
// Show/hide fields based on selection
document.querySelectorAll('[name="method"]').forEach(radio => {
radio.addEventListener('change', (e) => {
document.querySelectorAll('.payment-fields').forEach(el => {
el.style.display = 'none';
});
document.getElementById(e.target.value + '-fields').style.display = 'block';
});
});
// Handle form submission
document.getElementById('payment-form').addEventListener('submit', (e) => {
e.preventDefault();
const method = document.querySelector('[name="method"]:checked').value;
if (method === 'card') {
Omise.createToken('card', getCardData(), handleCallback);
} else if (method === 'promptpay') {
Omise.createSource('promptpay', {
amount: 50000,
currency: 'THB'
}, handleCallback);
} else if (method === 'truemoney') {
Omise.createSource('truemoney', {
amount: 50000,
currency: 'THB',
phone_number: document.getElementById('truemoney-phone').value
}, handleCallback);
}
});
function handleCallback(statusCode, response) {
if (statusCode === 200) {
submitToServer(response.id);
} else {
alert('Error: ' + response.message);
}
}
</script>
Reactコンポーネントの例
import { useState } from 'react';
function CreditCardForm({ amount, currency, onSuccess }) {
const [cardData, setCardData] = useState({
name: '',
number: '',
expiryMonth: '',
expiryYear: '',
cvv: ''
});
const [errors, setErrors] = useState({});
const [processing, setProcessing] = useState(false);
const handleInputChange = (field, value) => {
setCardData(prev => ({ ...prev, [field]: value }));
// Clear error when user types
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' }));
}
};
const formatCardNumber = (value) => {
return value
.replace(/\s/g, '')
.match(/.{1,4}/g)
?.join(' ') || value;
};
const validate = () => {
const newErrors = {};
if (!cardData.name.trim()) {
newErrors.name = 'Cardholder name is required';
}
const number = cardData.number.replace(/\s/g, '');
if (number.length < 13 || number.length > 19) {
newErrors.number = 'Invalid card number';
}
const month = parseInt(cardData.expiryMonth);
if (month < 1 || month > 12) {
newErrors.expiryMonth = 'Invalid month';
}
const year = parseInt(cardData.expiryYear);
if (year < new Date().getFullYear()) {
newErrors.expiryYear = 'Card expired';
}
if (cardData.cvv.length < 3) {
newErrors.cvv = 'Invalid CVV';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validate()) return;
setProcessing(true);
window.Omise.createToken('card', {
name: cardData.name,
number: cardData.number.replace(/\s/g, ''),
expiration_month: parseInt(cardData.expiryMonth),
expiration_year: parseInt(cardData.expiryYear),
security_code: cardData.cvv
}, async (statusCode, response) => {
if (statusCode === 200) {
try {
const result = await fetch('/api/charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: response.id,
amount,
currency
})
});
const data = await result.json();
if (data.authorize_uri) {
window.location.href = data.authorize_uri;
} else {
onSuccess(data);
}
} catch (error) {
alert('Payment error');
setProcessing(false);
}
} else {
alert('Card validation failed: ' + response.message);
setProcessing(false);
}
});
};
return (
<form onSubmit={handleSubmit} className="credit-card-form">
<div className="form-group">
<label>Cardholder Name</label>
<input
type="text"
value={cardData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
placeholder="John Doe"
/>
{errors.name && <span className="error">{errors.name}</span>}
</div>
<div className="form-group">
<label>Card Number</label>
<input
type="text"
value={cardData.number}
onChange={(e) => handleInputChange('number', formatCardNumber(e.target.value))}
placeholder="4242 4242 4242 4242"
maxLength="19"
/>
{errors.number && <span className="error">{errors.number}</span>}
</div>
<div className="form-row">
<div className="form-group">
<label>Expiry</label>
<div style={{ display: 'flex', gap: '10px' }}>
<input
type="text"
value={cardData.expiryMonth}
onChange={(e) => handleInputChange('expiryMonth', e.target.value.replace(/\D/g, ''))}
placeholder="MM"
maxLength="2"
style={{ width: '60px' }}
/>
<span>/</span>
<input
type="text"
value={cardData.expiryYear}
onChange={(e) => handleInputChange('expiryYear', e.target.value.replace(/\D/g, ''))}
placeholder="YYYY"
maxLength="4"
style={{ width: '80px' }}
/>
</div>
{(errors.expiryMonth || errors.expiryYear) && (
<span className="error">{errors.expiryMonth || errors.expiryYear}</span>
)}
</div>
<div className="form-group">
<label>CVV</label>
<input
type="text"
value={cardData.cvv}
onChange={(e) => handleInputChange('cvv', e.target.value.replace(/\D/g, ''))}
placeholder="123"
maxLength="4"
style={{ width: '80px' }}
/>
{errors.cvv && <span className="error">{errors.cvv}</span>}
</div>
</div>
<button type="submit" disabled={processing}>
{processing ? 'Processing...' : `Pay ${currency} ${(amount / 100).toFixed(2)}`}
</button>
</form>
);
}
export default CreditCardForm;
セキュリティのベストプラクティス
1. 入力のサニタイゼーション
// 数字以外の文字を削除
const cardNumber = value.replace(/\D/g, '');
// 数字のみに制限
const cvv = value.replace(/[^0-9]/g, '');
// カード番号の表示をフォーマット
const formatted = cardNumber.match(/.{1,4}/g)?.join(' ') || cardNumber;
2. 機密データを絶対にログに記録しない
// ❌ ダメ
console.log('カードデータ:', cardData);
// ✅ 良い
console.log('トークン作成:', token.id);
3. 成功後にフォームをクリア
function clearSensitiveFields() {
document.getElementById('card-number').value = '';
document.getElementById('cvv').value = '';
}
4. オートコンプリート属性を使用
<input type="text" autocomplete="cc-name" />
<input type="text" autocomplete="cc-number" />
<input type="text" autocomplete="cc-exp-month" />
<input type="text" autocomplete="cc-exp-year" />
<input type="text" autocomplete="cc-csc" />
よくある質問
カード番号を検証するにはどうすればよいですか?
Luhnアルゴリズムを使用します:
function isValidCardNumber(number) {
const digits = number.replace(/\D/g, '');
let sum = 0;
let isEven = false;
for (let i = digits.length - 1; i >= 0; i--) {
let digit = parseInt(digits[i]);
if (isEven) {
digit *= 2;
if (digit > 9) digit -= 9;
}
sum += digit;
isEven = !isEven;
}
return sum % 10 === 0;
}
将来の使用のためにカードを保存できますか?
3Dセキュアを処理するにはどうすればよいですか?
Omise.jsはトークン化のみを処理します。3Dセキュアの場合:
- サーバーでチャージを作成
authorize_uriが返された場合、ユーザーをリダイレクト- ユーザーが3Dセキュアを完了
- ユーザーが
return_uriに戻る
3Dセキュアガイドを参照してください。
カスタムバリデーションメッセージを使用できますか?
はい!コールバックでエラーを処理します:
Omise.createToken('card', cardData, (statusCode, response) => {
if (statusCode !== 200) {
showCustomError(response.code, response.message);
}
});
関連リソース
- インストール - Omise.jsのセットアップ
- 事前構築済みフォーム - すぐに使えるUIを使用
- APIリファレンス - 完全なAPIドキュメント
- テスト - テストカードと方法
- 3Dセキュア - 3DSの実装
次のステップ
- Omise.jsをインストール
- ✅ カスタムUIを構築(現在のページ)
- サーバーサイドでチャージを処理
- 統合をテスト
- 3Dセキュアを実装
- 本番稼働