事前認可(認可と実行)
Omiseの2段階決済フローを使用して、顧客の支払いを事前に認可し、後で資金を実行することで、柔軟な支払いタイミングと注文履行を実現します。
概要
事前認可(「認可と実行」とも呼ばれます)は、支払いをまず認可して資金を確認して保留し、その後、決済準備ができたときに実際の請求を実行する2段階のプロセスです。これは、受注生産製品、ホテル予約、レンタルサービス、および購入後に履行が行われるあらゆるシナリオに有用です。
主要な概念:
- 認可 - カードを確認して資金を保留(顧客はまだ請求されない)
- 実行 - 保留されている資金を回収(顧客に請求)
- 無効化 - 実行前に認可をキャンセル(資金をリリース)
- 自動実行 - デフォルトモード、認可と実行を直ちに実行
- 手動実行 - 2段階プロセス、明示的な実行が必要
事前認可 の使用時期
適切なユースケース
✅ 受注生産製品
- 注文時に認可
- 品目出荷時に実行
- 品目が利用不可の場合は無効化
✅ ホテル予約
- 予約時に認可
- チェックアウト時に実行
- 付加サービスの金額を調整
✅ レンタルサービス
- セキュリティデポジットを認可
- 損傷または手数料を実行
- 問題がない場合はリリース
✅ カスタムサービス
- 推定費用を認可
- 実際の最終費用を実行
- スコープの変更に対応
✅ 在庫確認
- 直ちに認可
- 在庫の可用性を確認
- 在庫がある場合は実行、ない場合は無効化
非推奨
❌ デジタル商品 - 直ちに配信、自動実行を使用 ❌ サブスクリプション - 通常の請求を使用(定期認可はできません) ❌ 低額商品 - 追加の複雑性が価値がない ❌ 直ちの履行 - 自動実行を 使用
仕組み
実装
ステップ1:認可を作成
- cURL
- Node.js
- PHP
- Python
- Ruby
- Go
- Java
- C#
curl https://api.omise.co/charges \
-u skey_test_YOUR_SECRET_KEY: \
-d "amount=100000" \
-d "currency=THB" \
-d "card=tokn_test_..." \
-d "capture=false"
const omise = require('omise')({
secretKey: 'skey_test_YOUR_SECRET_KEY'
});
const charge = await omise.charges.create({
amount: 100000, // THB 1,000.00
currency: 'THB',
card: tokenId,
capture: false, // 事前認可のみ
description: 'Pre-auth for Order #12345',
metadata: {
order_id: '12345'
}
});
console.log('課金ステータス:', charge.status); // 'pending'
console.log('認可:', charge.authorized); // true
console.log('支払済み:', charge.paid); // false
<?php
$charge = OmiseCharge::create(array(
'amount' => 100000,
'currency' => 'THB',
'card' => $tokenId,
'capture' => false
));
?>
import omise
omise.api_secret = 'skey_test_YOUR_SECRET_KEY'
charge = omise.Charge.create(
amount=100000,
currency='THB',
card=token_id,
capture=False
)
print('ステータス:', charge.status) # 'pending'
require 'omise'
Omise.api_key = 'skey_test_YOUR_SECRET_KEY'
charge = Omise::Charge.create({
amount: 100000,
currency: 'THB',
card: token_id,
capture: false
})
puts charge.status # 'pending'
charge, err := client.Charges().Create(&operations.CreateCharge{
Amount: 100000,
Currency: "THB",
Card: tokenId,
Capture: false,
})
Charge charge = client.charges().create(new Charge.CreateParams()
.amount(100000L)
.currency("THB")
.card(tokenId)
.capture(false));
var charge = await client.Charges.Create(new CreateChargeRequest
{
Amount = 100000,
Currency = "THB",
Card = tokenId,
Capture = false
});
レスポンス:
{
"object": "charge",
"id": "chrg_test_5rt6s9vah5lkvi1rh9c",
"amount": 100000,
"currency": "THB",
"status": "pending",
"authorized": true,
"paid": false,
"capture": false,
"capturable": true,
"expires_at": "2025-02-13T00:00:00Z"
}
ステップ2:資金を実行
// 注文 が履行された後
const capture = await omise.charges.capture('chrg_test_5rt6s9vah5lkvi1rh9c');
console.log('実行ステータス:', capture.status); // 'successful'
console.log('支払済み:', capture.paid); // true
異なる金額で実行(サポートされている場合):
// 認可額より少ない金額を実行(例:最終請求が低い)
const capture = await omise.charges.capture('chrg_test_...', {
capture_amount: 80000 // ฿1,000の認可額から฿800のみを実行
});
ステップ3:または認可を逆転/無効化
// 注文がキャンセルされた場合は認可をキャンセル
const reversed = await omise.charges.reverse('chrg_test_5rt6s9vah5lkvi1rh9c');
console.log('ステータス:', reversed.status); // 'reversed' or 'expired'
console.log('逆転時刻:', reversed.reversed_at);
完全な例
const express = require('express');
const omise = require('omise')({
secretKey: process.env.OMISE_SECRET_KEY
});
const app = express();
app.use(express.json());
// ステップ1:顧客が注文を発注
app.post('/checkout', async (req, res) => {
try {
const { tokenId, amount, orderId } = req.body;
// 支払いを認可(まだ実行しない)
const charge = await omise.charges.create({
amount: amount,
currency: 'THB',
card: tokenId,
capture: false,
description: `Pre-auth for Order #${orderId}`,
metadata: {
order_id: orderId,
authorized_at: new Date().toISOString()
}
});
if (charge.status === 'pending' && charge.authorized) {
// データベースに保存
await db.orders.create({
order_id: orderId,
charge_id: charge.id,
status: 'authorized',
amount: amount,
expires_at: charge.expires_at
});
res.json({
success: true,
message: '支払いが認可されました',
order_id: orderId
});
} else {
res.status(400).json({
error: '認可に失敗しました',
reason: charge.failure_message
});
}
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// ステップ2:注文を履行して実行
app.post('/orders/:orderId/ship', async (req, res) => {
try {
const { orderId } = req.params;
const { trackingNumber } = req.body;
// 注文を取得
const order = await db.orders.findOne({ order_id: orderId });
if (order.status !== 'authorized') {
return res.status(400).json({
error: '注文が認可された状態ではありません'
});
}
// 支払いを実行
const capture = await omise.charges.capture(order.charge_id);
if (capture.status === 'successful') {
// 注文を更新
await db.orders.update({
order_id: orderId,
status: 'captured',
shipped: true,
tracking_number: trackingNumber,
captured_at: new Date()
});
res.json({
success: true,
message: '支払いが実行され、注文が発送されました'
});
} else {
res.status(400).json({
error: '実行に失敗しました'
});
}
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// ステップ3:注文をキャンセルして資金をリリース
app.post('/orders/:orderId/cancel', async (req, res) => {
try {
const { orderId } = req.params;
const { reason } = req.body;
const order = await db.orders.findOne({ order_id: orderId });
if (order.status !== 'authorized') {
return res.status(400).json({
error: '注文をキャンセルできません'
});
}
// 認可を逆転
const reversed = await omise.charges.reverse(order.charge_id);
// 注文を更新
await db.orders.update({
order_id: orderId,
status: 'canceled',
cancellation_reason: reason,
canceled_at: new Date()
});
res.json({
success: true,
message: '認可が逆転され、資金がリリースされました'
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// バックグラウンドジョブ:有効期限が切れた認可を自動逆転
cron.schedule('0 * * * *', async () => {
const expiredOrders = await db.orders.find({
status: 'authorized',
expires_at: { $lt: new Date() }
});
for (const order of expiredOrders) {
try {
await omise.charges.reverse(order.charge_id);
await db.orders.update({
order_id: order.order_id,
status: 'expired'
});
console.log(`逆転された有効期限切れ注文: ${order.order_id}`);
} catch (error) {
console.error(`${order.order_id}の逆転に失敗:`, error);
}
}
});
app.listen(3000);
認可の有効期限
認可は実行されない場合は有効期限切れになります:
| カードネットワーク | 有効期限 |
|---|---|
| Visa | 7日 |
| Mastercard | 7日 |
| Amex | 7日 |
| JCB | 7日 |
自動有効期限切れ
実行されていない認可は、カードネットワークの時間制限後に自動的に有効期限切れになります。有効期限日を監視し、有効期限切れ前に実行してください。
// 認可がまもなく有効期限切れになるかを確認
function isExpiringSoon(charge) {
const expiryDate = new Date(charge.expires_at);
const now = new Date();
const daysUntilExpiry = (expiryDate - now) / (1000 * 60 * 60 * 24);
return daysUntilExpiry < 2; // 2日未満
}
// まもなく有効期限切れになる場合はアラート
if (isExpiringSoon(charge)) {
await sendExpiryAlert(order.id, charge.expires_at);
}
実行金額の調整
一部のシナリオでは異なる金額の実行を許可します:
実行を減らす(下方調整)
// ฿1,000を認可したが、最終費用は฿800
const capture = await omise.charges.capture('chrg_test_...', {
capture_amount: 80000 // ฿1,000ではなく฿800を実行
});
ユースケース:
- 最終請求が見積より低い
- 顧客が出荷前にいくつかの商品を返品
- プロモーション割引が適用
- 損傷または欠陥が発見
より多く実行(非サポート)
より多く実行できません
認可額より多い金額を実行することはできません。最終費用がより高い場合は、以下のいずれかを実行する必要があります:
- 差額に対して新しい課金を作成、または
- 元の認可を無効化して、正しい金額で新しい認可を作成
ベストプラクティス
1. 認可の有効期限を監視
// 毎日のcronジョブ
cron.schedule('0 8 * * *', async () => {
// 2日以内に有効期限切れになる認可を検索
const expiringCharges = await db.orders.find({
status: 'authorized',
expires_at: {
$gte: new Date(),
$lt: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000)
}
});
for (const order of expiringCharges) {
// 履行チームに通知
await sendSlackNotification({
message: `⚠️ 注文 ${order.order_id} の認可が2日以内に有効期限切れになります!`,
urgency: 'high'
});
// 遅延について顧客にメール
await sendEmailToCustomer(order.customer_email, {
template: 'order_delay',
order_id: order.order_id
});
}
});
2. 明確なコミュニケーション
// 認可時
await sendEmail({
to: customer.email,
subject: '注文確定 - 支払いが認可されました',
html: `
<h2>注文 #${orderId} が確定しました</h2>
<p>あなたの支払い ฿${amount / 100} を認可しました。</p>
<p><strong>重要:</strong> あなたのカードは注文が発送されたときにのみ請求されます。</p>
<ul>
<li>推定発送日: ${estimatedShipDate}</li>
<li>認可の有効期限: ${expiresAt}</li>
</ul>
<p>あなたのカードが請求されたときに確認メールが届きます。</p>
`
});
3. 発送時の自動実行
// 配送システムと統合
app.post('/webhooks/shipstation', async (req, res) => {
const shipment = req.body;
if (shipment.event === 'shipment_created') {
const order = await db.orders.findOne({
tracking_number: shipment.tracking_number
});
if (order && order.status === 'authorized') {
// 発送時に自動実行
await omise.charges.capture(order.charge_id);
await db.orders.update({
order_id: order.order_id,
status: 'captured',
captured_at: new Date()
});
// 顧客に通知
await sendShippedEmail(order.customer_email, {
tracking: shipment.tracking_number,
amount: order.amount
});
}
}
res.sendStatus(200);
});
4. 部分履行を処理
async function handlePartialShipment(orderId, shippedItems) {
const order = await db.orders.findOne({ order_id: orderId });
// 出荷された商品の金額を計算
const shippedAmount = shippedItems.reduce((sum, item) => {
return sum + (item.price * item.quantity);
}, 0);
// 出荷された商品のみを実行
await omise.charges.capture(order.charge_id, {
capture_amount: shippedAmount
});
// 残りの商品に対して新しい認可を作成
const remainingAmount = order.amount - shippedAmount;
if (remainingAmount > 0) {
const newCharge = await omise.charges.create({
amount: remainingAmount,
currency: 'THB',
customer: order.customer_id,
capture: false
});
await db.orders.create({
parent_order_id: orderId,
charge_id: newCharge.id,
status: 'authorized',
amount: remainingAmount
});
}
}
5. 在庫確認の猶予期間
async function processOrder(orderId) {
const order = await db.orders.findOne({ order_id: orderId });
// 在庫を確認
const inStock = await checkInventory(order.items);
if (inStock) {
// 在庫がある場合は直ちに実行
await omise.charges.capture(order.charge_id);
await fulfillOrder(orderId);
} else {
// 再入荷に24時間の猶予を与える
setTimeout(async () => {
const stillInStock = await checkInventory(order.items);
if (stillInStock) {
await omise.charges.capture(order.charge_id);
await fulfillOrder(orderId);
} else {
// キャンセルして払い戻し
await omise.charges.reverse(order.charge_id);
await notifyCustomer(order.customer_email, 'out_of_stock');
}
}, 24 * 60 * 60 * 1000); // 24時間
}
}
テスト
テストモード事前認可
テストカードを使用して事前認可実装を検証します:
const omise = require('omise')({
secretKey: 'skey_test_YOUR_SECRET_KEY'
});
// 認可をテスト
const charge = await omise.charges.create({
amount: 100000,
currency: 'THB',
card: 'tokn_test_5rt6s9vah5lkvi1rh9c', // テストカードトークン
capture: false,
metadata: {
test_scenario: 'pre_authorization'
}
});
console.log('ステータス:', charge.status); // 'pending'
console.log('認可:', charge.authorized); // true
console.log('実行可能:', charge.capturable); // true
テストシナリオ
1. 成功した認可と実行
// ステップ1:認可
const auth = await omise.charges.create({
amount: 100000,
currency: 'THB',
card: 'tokn_test_4242', // 成功カード
capture: false
});
console.log('認可が成功:', auth.status === 'pending');
// ステップ2:実行
const capture = await omise.charges.capture(auth.id);
console.log('実行が成功:', capture.status === 'successful');
console.log('顧客に請求:', capture.paid); // true
2. 認可と無効化
// 認可
const auth = await omise.charges.create({
amount: 100000,
currency: 'THB',
card: 'tokn_test_4242',
capture: false
});
// 認可をキャンセル
const reversed = await omise.charges.reverse(auth.id);
console.log('無効化:', reversed.status === 'reversed');