Skip to main content

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โ€‹

Feature3DS 1.0 (Legacy)3DS 2.0 (Current)
User ExperiencePop-up, clunkyIn-app, seamless
Mobile SupportPoorExcellent
Data SharedLimitedRich context
FrictionHighLow (often frictionless)
AuthenticationPasswordBiometric, PIN, OTP
Success RateLowerHigher
info

Omise supports 3D Secure 2.0 by default, providing the best user experience and highest approval rates.

How 3D Secure Worksโ€‹

Customer Experience:

  1. Customer enters card details at checkout
  2. If 3DS required, redirected to issuer's authentication page
  3. Customer authenticates with:
    • Biometric (fingerprint, Face ID)
    • PIN or password
    • One-time password (SMS/app)
  4. Returns to merchant site
  5. Payment completes or fails based on authentication

Completion time: 10-30 seconds (seamless with biometrics)

Implementationโ€‹

Omise automatically triggers 3DS when required:

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

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 Number3DS BehaviorResult
4242424242424242No 3DSSuccess
40000000000032203DS required, succeedsSuccess
40000000000092353DS required, failsDeclined
40000082600031783DS, insufficient fundsDeclined

Test Flowโ€‹

  1. Use 3DS test card
  2. Redirected to test 3DS page
  3. Click "Success" or "Fail" button
  4. 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).

Next Stepsโ€‹

  1. Implement automatic 3DS in your integration
  2. Add return_uri to all card charges
  3. Handle 3DS redirect and return flow
  4. Test with 3DS test cards
  5. Set up webhook handling
  6. Monitor 3DS success rates
  7. Go live