3D Secure
Add bank-issued second-factor authentication to reduce fraud by 70%+ and shift chargeback liability to card issuers.
Overviewโ
3D Secure (3DS) is an additional security layer for online credit and debit card transactions that requires cardholders to complete an extra verification step with their card-issuing bank. This authentication significantly reduces fraud and shifts liability for chargebacks from merchants to banks.
Key Benefits:
- โ 70%+ fraud reduction - Extra authentication prevents unauthorized use
- โ Liability shift - Banks assume chargeback responsibility for authenticated transactions
- โ Lower chargeback rates - Reduces disputes and associated fees
- โ Frictionless option - 3DS2 allows instant approval for low-risk transactions
- โ Required for compliance - Mandatory for certain business types and regions
3D Secure 1 vs 3D Secure 2โ
| Feature | 3DS1 (Deprecated) | 3DS2 (Current Standard) |
|---|---|---|
| Status | Deprecated October 2022 | Active and required |
| Authentication Methods | OTP/SMS only | OTP, SMS, biometrics, facial recognition |
| Frictionless Flow | Not available | Available for low-risk transactions |
| Mobile Support | Limited | Full in-app support |
| Data Sharing | Limited | Rich transaction context (device, location, history) |
| User Experience | Always requires interaction | Smart risk-based decisions |
| Authorization Rate | Lower | Higher (frictionless reduces cart abandonment) |
3D Secure 1 was deprecated in October 2022. All implementations should use 3D Secure 2 (3DS2).
How 3DS2 Worksโ
Frictionless Flowโ
- Bank analyzes transaction risk using rich data (device ID, geolocation, purchase history)
- If risk is low enough, transaction approved instantly without customer interaction
- Same liability protection as challenge flow
- Significantly better user experience - no redirect, faster checkout
Challenge Flowโ
- Used for higher-risk transactions requiring additional verification
- Customer redirected to bank's authentication page
- Multiple authentication options:
- OTP (One-Time Password via SMS)
- Biometric (fingerprint, face ID)
- Bank app push notification
- Typically takes 1-3 minutes
- May extend to 10 minutes if first-time enrollment
When to Enable 3D Secureโ
Mandatory Forโ
- โ Travel & accommodation - Hotels, airlines, booking platforms
- โ Digital goods - Music, movies, software, games
- โ Virtual items - Game currency, in-app purchases
- โ Prepaid cards - Gift cards, top-up services
- โ High-risk merchants - As determined by fraud analysts
Recommended Forโ
- High-value transactions (> THB 10,000 or equivalent)
- Businesses with chargeback rates > 0.3%
- International card transactions
- First-time customers
- Suspicious transaction patterns
Not Recommended Forโ
- โ Recurring subscriptions - Prevents automatic charging
- โ One-click repeat purchases - Requires interaction each time
- โ Low-value transactions - May reduce conversion on small purchases
For subscriptions: Use 3DS for initial card setup, then charge saved customer ID without 3DS for subsequent payments (subject to bank rules).
Implementation Guideโ
Step 1: Enable 3DS on Your Accountโ
Contact Omise support to enable 3D Secure:
Email: support@omise.co
Subject: Enable 3D Secure for [Your Account]
Step 2: Create Token (Standard Process)โ
// Client-side: Create token as usual
Omise.setPublicKey("pkey_test_YOUR_KEY");
Omise.createToken("card", {
name: "John Doe",
number: "4242424242424242",
expiration_month: 12,
expiration_year: 2027,
security_code: "123"
}, function(statusCode, response) {
if (statusCode === 200) {
// Send token to server
submitToServer(response.id);
}
});
Step 3: Create Charge with return_uriโ
The return_uri parameter is required for 3DS:
- cURL
- Node.js
- PHP
- Python
curl https://api.omise.co/charges \
-u skey_test_YOUR_SECRET_KEY: \
-d "amount=100000" \
-d "currency=THB" \
-d "card=tokn_test_..." \
-d "return_uri=https://yourdomain.com/payment/callback"
const omise = require('omise')({
secretKey: 'skey_test_YOUR_SECRET_KEY'
});
const charge = await omise.charges.create({
amount: 100000,
currency: 'THB',
card: tokenId,
return_uri: 'https://yourdomain.com/payment/callback'
});
// Check if 3DS authentication required
if (charge.authorize_uri) {
// Redirect customer to authorize_uri for authentication
res.redirect(charge.authorize_uri);
} else {
// Charge completed without 3DS (frictionless)
handleSuccess(charge);
}
<?php
$charge = OmiseCharge::create(array(
'amount' => 100000,
'currency' => 'THB',
'card' => $tokenId,
'return_uri' => 'https://yourdomain.com/payment/callback'
));
if (isset($charge['authorize_uri'])) {
// Redirect to 3DS authentication
header('Location: ' . $charge['authorize_uri']);
exit;
} else {
// Charge successful (frictionless flow)
handleSuccess($charge);
}
?>
import omise
omise.api_secret = 'skey_test_YOUR_SECRET_KEY'
charge = omise.Charge.create(
amount=100000,
currency='THB',
card=token_id,
return_uri='https://yourdomain.com/payment/callback'
)
if charge.authorize_uri:
# Redirect to 3DS page
return redirect(charge.authorize_uri)
else:
# Frictionless success
return handle_success(charge)
Step 4: Handle Customer Redirectโ
If authorize_uri is present in the response, redirect customer:
// Server-side
app.post('/create-charge', async (req, res) => {
const charge = await omise.charges.create({
amount: req.body.amount,
currency: 'THB',
card: req.body.token,
return_uri: 'https://yourdomain.com/payment/callback',
metadata: {
order_id: req.body.order_id
}
});
if (charge.authorize_uri) {
// 3DS authentication required
res.json({
requires_3ds: true,
authorize_uri: charge.authorize_uri,
charge_id: charge.id
});
} else {
// Frictionless - charge completed
res.json({
requires_3ds: false,
status: charge.status,
charge_id: charge.id
});
}
});
// Client-side
fetch('/create-charge', {
method: 'POST',
body: JSON.stringify({ token, amount, order_id })
})
.then(res => res.json())
.then(data => {
if (data.requires_3ds) {
// Redirect to 3DS authentication
window.location.href = data.authorize_uri;
} else {
// Payment complete
window.location.href = '/payment-success';
}
});
Step 5: Handle Callbackโ
After authentication, customer is redirected back to your return_uri:
app.get('/payment/callback', async (req, res) => {
// Retrieve charge status
const chargeId = req.query.charge_id || req.session.charge_id;
const charge = await omise.charges.retrieve(chargeId);
if (charge.status === 'successful') {
// 3DS authentication passed
await processOrder(charge.metadata.order_id);
res.redirect('/payment-success?order=' + charge.metadata.order_id);
} else if (charge.status === 'failed') {
// 3DS authentication failed
res.redirect('/payment-failed?reason=' + encodeURIComponent(charge.failure_message));
} else {
// Still pending - check again or use webhooks
res.redirect('/payment-pending');
}
});
Step 6: Implement Webhook Handlerโ
For reliable status updates, use webhooks:
app.post('/webhooks/omise', (req, res) => {
const event = req.body;
if (event.key === 'charge.complete') {
const charge = event.data;
if (charge.status === 'successful') {
// Process successful payment
processOrder(charge.metadata.order_id);
} else if (charge.status === 'failed') {
// Handle failed payment
handleFailedPayment(charge.metadata.order_id, charge.failure_message);
}
}
res.sendStatus(200);
});
Complete Implementation Exampleโ
<!DOCTYPE html>
<html>
<head>
<title>3DS Checkout</title>
<script src="https://cdn.omise.co/omise.js"></script>
</head>
<body>
<form id="payment-form">
<input type="text" id="card-name" placeholder="Card Holder" required />
<input type="text" id="card-number" placeholder="Card Number" required />
<input type="text" id="expiry-month" placeholder="MM" required />
<input type="text" id="expiry-year" placeholder="YYYY" required />
<input type="text" id="cvv" placeholder="CVV" required />
<button type="submit">Pay THB 1,000</button>
</form>
<script>
Omise.setPublicKey("pkey_test_YOUR_KEY");
document.getElementById('payment-form').addEventListener('submit', async (e) => {
e.preventDefault();
// Step 1: Create token
Omise.createToken("card", {
name: document.getElementById('card-name').value,
number: document.getElementById('card-number').value,
expiration_month: parseInt(document.getElementById('expiry-month').value),
expiration_year: parseInt(document.getElementById('expiry-year').value),
security_code: document.getElementById('cvv').value
}, async (statusCode, response) => {
if (statusCode !== 200) {
alert('Error: ' + response.message);
return;
}
// Step 2: Create charge on server
const result = await fetch('/create-charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: response.id,
amount: 100000,
order_id: 'ORD-12345'
})
}).then(r => r.json());
// Step 3: Handle response
if (result.requires_3ds) {
// Redirect to 3DS authentication
window.location.href = result.authorize_uri;
} else {
// Frictionless - payment complete
window.location.href = '/payment-success';
}
});
});
</script>
</body>
</html>
Trade-offs & Considerationsโ
Advantagesโ
- Fraud reduction: 70%+ decrease in unauthorized transactions
- Liability shift: Banks assume chargeback liability
- Lower fees: Fewer disputes = lower dispute fees
- Compliance: Required for certain business types
- Customer trust: Professional security = increased confidence
Disadvantagesโ
- Conversion impact: Some customers abandon at redirect (frictionless helps)
- No recurring payments: Cannot automatically charge saved cards
- User friction: Challenge flow adds 1-3 minutes to checkout
- Technical complexity: Additional redirect handling required
- International variations: Bank implementation quality varies
Mitigation Strategiesโ
Reduce Cart Abandonment:
// Show clear messaging before redirect
function initiate3DS(authorizeUri) {
const modal = showModal({
title: "Secure Payment Verification",
message: "You'll be redirected to your bank for verification. This keeps your payment secure.",
button: "Continue to Verification"
});
modal.onConfirm = () => {
window.location.href = authorizeUri;
};
}
Handle Recurring Payments:
// First charge: Use 3DS
const firstCharge = await omise.charges.create({
amount: 29900,
currency: 'THB',
card: tokenId,
return_uri: 'https://example.com/callback',
metadata: { type: 'subscription_signup' }
});
// Save customer for future charges
const customer = await omise.customers.create({
email: 'customer@example.com',
card: tokenId
});
// Subsequent charges: No 3DS (use customer ID)
const recurringCharge = await omise.charges.create({
amount: 29900,
currency: 'THB',
customer: customer.id,
// No return_uri = no 3DS required
});
Testing 3D Secureโ
Enable Test Mode 3DSโ
Contact support to enable 3DS in test mode:
Email: support@omise.co
Subject: Enable 3DS for Test Mode
Test Cardsโ
Use these cards for testing different 3DS scenarios:
3DS testing requires a 3DS-enabled test account. Contact support@omise.co to enable.
Standard Success Cards (no 3DS issues):
| Card Number | Brand | Expected Result |
|---|---|---|
| 4242 4242 4242 4242 | Visa | Frictionless approval |
| 5555 5555 5555 4444 | Mastercard | Frictionless approval |
3DS Enrollment Failure Cards:
| Card Number | Brand | Expected Result |
|---|---|---|
| 4111 1111 1115 0002 | Visa | 3DS enrollment fails |
| 5555 5511 1112 0002 | Mastercard | 3DS enrollment fails |
| 3530 1111 1110 0002 | JCB | 3DS enrollment fails |
3DS Authentication Failure Cards:
| Card Number | Brand | Expected Result |
|---|---|---|
| 4111 1111 1114 0003 | Visa | 3DS authentication fails |
| 5555 5511 1111 0003 | Mastercard | 3DS authentication fails |
| 3771 3816 1111 003 | Amex | 3DS authentication fails |
Test Authentication Flowโ
- Create charge with test card and return_uri
- If
authorize_uripresent, visit the URL - Complete mock authentication on test page
- Verify redirect back to return_uri
- Check final charge status
# Test charge with 3DS
curl https://api.omise.co/charges \
-u skey_test_YOUR_KEY: \
-d "amount=100000" \
-d "currency=THB" \
-d "card=tokn_test_..." \
-d "return_uri=https://example.com/callback"
Common Issues & Troubleshootingโ
Issue: "3d secure is requested, but return URI is not set"โ
Cause: 3DS enabled on account but return_uri not provided
Solution:
// Always include return_uri when 3DS is enabled
const charge = await omise.charges.create({
amount: 100000,
currency: 'THB',
card: tokenId,
return_uri: 'https://yourdomain.com/callback' // Required!
});
Issue: Customer abandoned at 3DS pageโ
Causes:
- Confusing bank authentication page
- Long authentication time
- Customer doesn't know bank password
Solutions:
- Show clear messaging before redirect
- Set customer expectations ("You'll verify with your bank")
- Provide support contact info
- Implement timeout handling
Issue: return_uri not being calledโ
Causes:
- Customer closed browser
- Bank redirect failed
- Incorrect return_uri URL
Solutions:
- Use webhooks as backup (
charge.completeevent) - Allow customers to check order status
- Implement order status page with charge ID lookup
Issue: Frictionless not workingโ
Causes:
- Insufficient transaction data sent to bank
- Customer new to merchant
- High-risk transaction characteristics
Solutions:
- Provide complete billing address
- Include customer email and phone
- Build transaction history over time
Best Practicesโ
-
Provide Clear Communication
<div class="3ds-notice">
<p>For your security, you'll be asked to verify this payment with your bank.</p>
<p>This typically takes less than 1 minute.</p>
</div> -
Implement Proper Error Handling
if (charge.failure_code === '3d_secure_authentication_failed') {
showMessage('Bank authentication failed. Please try again or use a different card.');
} -
Use Webhooks for Reliability
- Don't rely solely on redirect callbacks
- Implement
charge.completewebhook handler - Update order status from webhook events
-
Optimize for Frictionless
- Collect complete customer data
- Provide billing address
- Include phone and email
- Build positive payment history
-
Handle Timeouts Gracefully
// Set reasonable timeout
setTimeout(() => {
if (!paymentConfirmed) {
showMessage('Payment verification is taking longer than expected. Check your order status.');
}
}, 5 * 60 * 1000); // 5 minutes -
Test Thoroughly
- Test both frictionless and challenge flows
- Test failure scenarios
- Test on mobile devices
- Test with actual test banking apps
FAQโ
Is 3D Secure mandatory?
3D Secure is mandatory for certain business types (travel, digital goods, gaming) as determined by Omise's fraud analysts. For other merchants, it's optional but recommended for high-value or high-risk transactions.
Does 3D Secure guarantee no chargebacks?
No, but it significantly reduces them. With successful 3DS authentication, liability shifts to the issuing bank, meaning chargebacks are less likely to be approved. However, banks may still dispute 3DS transactions in rare cases.
Can I use 3D Secure for subscriptions?
Not directly for automatic recurring payments. However, you can:
- Use 3DS for initial card setup
- Save card as customer
- Charge customer ID without 3DS for recurring payments (subject to bank rules)
What's the difference between frictionless and challenge flow?
Frictionless: Bank approves instantly based on risk analysis - no customer interaction needed. Better UX.
Challenge: Bank requires additional verification (OTP, biometric) - customer must interact. More secure but slower.
Banks decide which flow to use based on transaction risk.
How long does 3DS authentication take?
- Frictionless: Instant (< 1 second)
- Challenge: 1-3 minutes typically
- First-time enrollment: Up to 10 minutes
Most modern implementations use frictionless flow for low-risk transactions.
Does 3D Secure work on mobile?
Yes! 3DS2 is fully optimized for mobile with:
- In-app authentication (no browser redirect)
- Biometric options (fingerprint, Face ID)
- Banking app deep links
- Better mobile UX than 3DS1
Related Resourcesโ
- Fraud Protection - Additional security layers
- Disputes - Handle chargebacks
- Testing Guide - Test 3DS integration
- Webhooks - Reliable payment notifications
- Security Best Practices