3D Secure
Implement 3D Secure 2.0 authentication to reduce fraud, shift liability, and comply with Strong Customer Authentication (SCA) requirements in Europe and beyond.
Overviewโ
3D Secure (3DS) is an authentication protocol that adds an extra layer of security to online card transactions. When enabled, customers verify their identity with their card issuer before completing payment.
Key Benefits:
- ๐ Reduced fraud - Extra authentication step
- ๐ก๏ธ Liability shift - Fraud liability shifts to card issuer
- โ SCA compliance - Required for European payments
- ๐ Higher approval rates - Issuers trust authenticated transactions
- ๐ณ Supports all cards - Visa, Mastercard, JCB
3D Secure Brands:
- Visa: Visa Secure (formerly Verified by Visa)
- Mastercard: Mastercard Identity Check (formerly Mastercard SecureCode)
- JCB: J/Secure
3D Secure 1 vs 3D Secure 2โ
| Feature | 3DS 1.0 (Legacy) | 3DS 2.0 (Current) |
|---|---|---|
| User Experience | Pop-up, clunky | In-app, seamless |
| Mobile Support | Poor | Excellent |
| Data Shared | Limited | Rich context |
| Friction | High | Low (often frictionless) |
| Authentication | Password | Biometric, PIN, OTP |
| Success Rate | Lower | Higher |
Omise supports 3D Secure 2.0 by default, providing the best user experience and highest approval rates.
How 3D Secure Worksโ
Customer Experience:
- Customer enters card details at checkout
- If 3DS required, redirected to issuer's authentication page
- Customer authenticates with:
- Biometric (fingerprint, Face ID)
- PIN or password
- One-time password (SMS/app)
- Returns to merchant site
- Payment completes or fails based on authentication
Completion time: 10-30 seconds (seamless with biometrics)
Implementationโ
Automatic 3DS (Recommended)โ
Omise automatically triggers 3DS when required:
- Node.js
- PHP
- Python
const omise = require('omise')({
publicKey: 'pkey_test_YOUR_PUBLIC_KEY',
secretKey: 'skey_test_YOUR_SECRET_KEY'
});
// Create charge with card token
const charge = await omise.charges.create({
amount: 100000,
currency: 'THB',
card: cardToken, // From Omise.js
return_uri: 'https://yourdomain.com/payment/callback'
});
// Check if 3DS required
if (charge.authorize_uri) {
// Redirect customer to 3DS authentication
res.redirect(charge.authorize_uri);
} else if (charge.status === 'successful') {
// Payment successful without 3DS
res.redirect('/success');
}
<?php
$charge = OmiseCharge::create(array(
'amount' => 100000,
'currency' => 'THB',
'card' => $cardToken,
'return_uri' => 'https://yourdomain.com/payment/callback'
));
if ($charge['authorize_uri']) {
// Redirect to 3DS
header('Location: ' . $charge['authorize_uri']);
} else if ($charge['status'] === 'successful') {
// Success without 3DS
header('Location: /success');
}
?>
import omise
charge = omise.Charge.create(
amount=100000,
currency='THB',
card=card_token,
return_uri='https://yourdomain.com/payment/callback'
)
if charge.authorize_uri:
# Redirect to 3DS
return redirect(charge.authorize_uri)
elif charge.status == 'successful':
# Success without 3DS
return redirect('/success')
Handle Return from 3DSโ
app.get('/payment/callback', async (req, res) => {
try {
const chargeId = req.query.charge_id;
const charge = await omise.charges.retrieve(chargeId);
if (charge.status === 'successful') {
// 3DS authentication successful
await processOrder(charge.metadata.order_id);
res.redirect('/payment-success');
} else if (charge.status === 'failed') {
// Authentication or payment failed
res.redirect(`/payment-failed?reason=${charge.failure_message}`);
} else {
// Pending (rare)
res.redirect('/payment-pending');
}
} catch (error) {
res.redirect('/payment-error');
}
});
When 3DS is Requiredโ
Mandatory (Always Required)โ
European Economic Area (EEA):
- All consumer payments require Strong Customer Authentication (SCA)
- Covers EU + Iceland, Liechtenstein, Norway
- Merchants must implement 3DS or face declining cards
Some Banks/Regions:
- India (RBI mandate)
- Parts of Asia-Pacific
- Varies by issuer policy
Optional (Risk-Based)โ
Card Issuers May Require 3DS For:
- High-value transactions
- High-risk merchants
- Suspicious patterns
- Customer's first transaction with merchant
- Cross-border payments
Frictionless vs Challenge:
- Frictionless: Issuer approves without customer action (80%+ of 3DS 2.0)
- Challenge: Customer must authenticate (20%)
Complete Implementation Exampleโ
const express = require('express');
const omise = require('omise')({
publicKey: process.env.OMISE_PUBLIC_KEY,
secretKey: process.env.OMISE_SECRET_KEY
});
const app = express();
app.use(express.json());
// Step 1: Create charge
app.post('/checkout', async (req, res) => {
try {
const { token, amount, currency, order_id } = req.body;
const charge = await omise.charges.create({
amount: amount,
currency: currency,
card: token, // From Omise.js
return_uri: `${process.env.BASE_URL}/payment/callback`,
metadata: {
order_id: order_id
}
});
if (charge.authorize_uri) {
// 3DS required - return redirect URL
res.json({
requires_authentication: true,
authorize_uri: charge.authorize_uri,
charge_id: charge.id
});
} else if (charge.status === 'successful') {
// Success without 3DS
await processOrder(order_id);
res.json({
requires_authentication: false,
status: 'successful',
charge_id: charge.id
});
} else {
// Failed
res.status(400).json({
error: charge.failure_message
});
}
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Step 2: Handle 3DS return
app.get('/payment/callback', async (req, res) => {
try {
const charge = await omise.charges.retrieve(req.query.charge_id);
if (charge.status === 'successful') {
await processOrder(charge.metadata.order_id);
res.redirect(`/order-confirmation?order=${charge.metadata.order_id}`);
} else {
res.redirect(`/payment-failed?error=${charge.failure_code}`);
}
} catch (error) {
res.redirect('/payment-error');
}
});
// Step 3: Webhook (recommended)
app.post('/webhooks/omise', async (req, res) => {
const event = req.body;
if (event.key === 'charge.complete') {
const charge = event.data;
if (charge.status === 'successful') {
await processOrder(charge.metadata.order_id);
}
}
res.sendStatus(200);
});
app.listen(3000);
Client-Side Handlingโ
// Submit payment form
document.getElementById('payment-form').addEventListener('submit', async (e) => {
e.preventDefault();
// Create token with Omise.js (not shown)
const token = await createCardToken();
// Create charge
const response = await fetch('/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: token,
amount: 100000,
currency: 'THB',
order_id: '12345'
})
});
const data = await response.json();
if (data.requires_authentication) {
// Redirect to 3DS authentication
window.location = data.authorize_uri;
} else if (data.status === 'successful') {
// Success without 3DS
window.location = '/payment-success';
} else {
// Failed
showError(data.error);
}
});
Testing 3D Secureโ
Test Cardsโ
| Card Number | 3DS Behavior | Result |
|---|---|---|
| 4242424242424242 | No 3DS | Success |
| 4000000000003220 | 3DS required, succeeds | Success |
| 4000000000009235 | 3DS required, fails | Declined |
| 4000008260003178 | 3DS, insufficient funds | Declined |
Test Flowโ
- Use 3DS test card
- Redirected to test 3DS page
- Click "Success" or "Fail" button
- Returned to return_uri with result
See Testing Guide for full list.
Best Practicesโ
1. Always Provide return_uriโ
// Required for 3DS redirect
const charge = await omise.charges.create({
// ... other params
return_uri: 'https://yourdomain.com/payment/callback' // Must be HTTPS
});
2. Handle All Charge Statusesโ
switch (charge.status) {
case 'successful':
// Payment complete
break;
case 'failed':
// Show error, allow retry
break;
case 'pending':
// Wait for webhook (rare)
break;
case 'expired':
// Customer didn't complete 3DS
break;
}
3. Use HTTPSโ
3DS requires HTTPS for return_uri:
โ
https://yourdomain.com/callback
โ http://yourdomain.com/callback
4. Implement Webhooksโ
Don't rely solely on return_uri:
// Webhook is source of truth
app.post('/webhooks/omise', handleWebhook);
// Return URI is for customer UX only
app.get('/payment/callback', showCustomerResult);
5. Show Loading Stateโ
// Show loading during 3DS redirect
window.addEventListener('beforeunload', () => {
showLoader('Redirecting to bank...');
});
6. Handle Timeoutโ
3DS typically expires after 10 minutes:
if (charge.status === 'expired') {
showMessage('Authentication timed out. Please try again.');
allowRetry();
}
Common Issuesโ
Issue: return_uri not being calledโ
Causes:
- Customer closed browser during 3DS
- Customer clicked back button
- Network error
Solution: Use webhooks as primary notification
Issue: 3DS popup blockedโ
Cause: Browser blocking redirect
Solution: Ensure redirect happens from user interaction (form submit)
Issue: High 3DS decline rateโ
Causes:
- Customer abandons during 3DS
- Customer enters wrong OTP
- Issuer decline
Solutions:
- Optimize 3DS UX (use 3DS 2.0)
- Clear instructions
- Allow retry with different card
FAQโ
Is 3D Secure mandatory?
Yes for:
- European Economic Area (EEA) payments - SCA requirement
- India - RBI mandate
- Some Asia-Pacific regions
Optional for:
- Other regions (but recommended for fraud protection)
- High-risk transactions (issuer may require)
Does 3DS reduce conversion?
3DS 1.0: Yes, significant drop (10-20%) 3DS 2.0: Minimal impact (1-5%) due to:
- Frictionless authentication (80%+ of cases)
- Biometric authentication (fast and easy)
- Better mobile experience
What is liability shift?
With successful 3DS authentication, fraud liability shifts from merchant to card issuer. Without 3DS, merchant bears fraud liability.
Can customers skip 3DS?
No. When 3DS is required (by regulation or issuer), it cannot be skipped. Payment will fail if customer doesn't complete authentication.
Does 3DS work with saved cards?
Yes. Saved card tokens can be charged with 3DS. Customer must authenticate each payment (3DS doesn't exempt recurring payments from SCA in most cases).
Related Resourcesโ
- Charging Cards - Card payment guide
- Collecting Cards - Secure card collection
- Fraud Protection - Additional fraud tools
- Testing - 3DS test cards
- Webhooks - Handle payment notifications
Next Stepsโ
- Implement automatic 3DS in your integration
- Add return_uri to all card charges
- Handle 3DS redirect and return flow
- Test with 3DS test cards
- Set up webhook handling
- Monitor 3DS success rates
- Go live