Appearance
3D Secure
Collect 3DS authentication data with PAAY's JavaScript SDK and pass it through to /api/transaction. Use this when you need full control over your own payment form instead of the Tokenizer's built-in 3DS.
When to Use
| Approach | Best For |
|---|---|
| Tokenizer 3DS | Standard integrations — card collection, tokenization, and 3DS handled in one iframe. See 3DS Tokenizer. |
| PAAY SDK pass-through (this guide) | Custom forms where you need full control, are already PCI-compliant (SAQ D), or are integrating 3DS into an existing API-based flow. |
Overview
The pass-through flow has three stages:
- Authenticate — PAAY's JS SDK authenticates the cardholder with the issuer on your checkout page.
- Collect — PAAY returns the 3DS values (CAVV, ECI, XID/dsTransId) to your frontend.
- Submit — Your server sends those values along with the card to
/api/transaction.
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Your Checkout │──▶│ PAAY 3DS │──▶│ Card Issuer │──▶│ 3DS Result │
│ Page │ │ JS SDK │ │ (ACS) │ │ (CAVV/ECI) │
└──────────────┘ └──────────────┘ └──────────────┘ └──────┬───────┘
│
▼
┌──────────────┐
│ Your Server │
│ │
│ POST to │
│ Gateway │
│ /api/ │
│ transaction │
└──────────────┘Prerequisites
Before you start, make sure you have:
- PAAY account — Active account with PAAY (3DS Integrator). Contact your account rep or info@paay.co.
- PAAY API key — From the PAAY Portal. Separate from your Gateway API key.
- Gateway API key — Your secret key (
api_XXXX) for server-side calls. - PCI compliance — Card data is collected in your form, so your environment must meet PCI requirements (typically SAQ D).
Steps
1. Form
Add card inputs tagged with data-threeds attributes so the PAAY SDK can read them, plus hidden fields for the transaction ID and amount.
html
<form id="checkout-form" action="/api/charge" method="post">
<!-- Cardholder Information -->
<label>Card Number</label>
<input type="text" name="cardNumber" maxlength="16" data-threeds="pan" />
<label>Expiration Month</label>
<select name="cardMonth" data-threeds="month">
<option value="">--</option>
<option value="01">01</option>
<option value="02">02</option>
<!-- ... -->
<option value="12">12</option>
</select>
<label>Expiration Year</label>
<select name="cardYear" data-threeds="year">
<option value="">--</option>
<option value="25">2025</option>
<option value="26">2026</option>
<!-- ... -->
<option value="30">2030</option>
</select>
<label>CVV</label>
<input type="text" name="cvv" maxlength="4" />
<!-- Hidden 3DS fields -->
<input type="hidden" name="x_transaction_id" value="" data-threeds="id" />
<input type="hidden" name="paayAmount" value="25.00" data-threeds="amount" />
<!-- 3DS results will be injected here by the SDK -->
<!-- (cavv, eci, xid are auto-appended when appendForm is true) -->
<button type="submit" id="pay-button">Pay $25.00</button>
</form>Form Attributes
| Attribute | Input | Description |
|---|---|---|
data-threeds="pan" | Card number | Full credit card number. |
data-threeds="month" | Expiration month | 1 or 2 digit month (e.g. 01, 12). |
data-threeds="year" | Expiration year | 2 digit year (e.g. 25, 30). |
data-threeds="id" | Transaction ID | Unique identifier for this 3DS session. Auto-generated below. |
data-threeds="amount" | Transaction amount | Decimal amount (e.g. 25.00). |
2. Initialize SDK
Load the PAAY library and initialize the SDK before the closing </body> tag.
html
<!-- PAAY 3DS JavaScript SDK -->
<script src="https://cdn.3dsintegrator.com/threeds.min.latest.js"></script>
<!-- Generate a unique transaction ID for this 3DS session -->
<script>
var idInput = document.querySelector('[data-threeds=id]');
idInput.value = 'id-' + Math.random().toString(36).substr(2, 16);
</script>
<!-- Initialize the PAAY ThreeDS SDK -->
<script>
var tds = new ThreeDS(
'checkout-form', // Your form's ID attribute
'YOUR_PAAY_API_KEY', // Your PAAY API key from the PAAY Portal
null, // Pass null to let the SDK generate the JWT
{
autoSubmit: false, // We'll trigger authentication manually
endpoint: 'https://api-sandbox.3dsintegrator.com/v2.2', // Sandbox endpoint
verbose: true // Enable console logging for debugging
}
);
</script>Constructor Parameters
| Parameter | Description |
|---|---|
formId | The id of the <form> element with your card inputs. |
apiKey | Your PAAY API key from the PAAY Portal. |
jwt | Pass null to let the SDK generate one, or provide your own server-generated JWT. |
options | Configuration object — see below. |
SDK Options
| Option | Default | Description |
|---|---|---|
autoSubmit | true | Auto-trigger authentication as the customer fills in card fields. Set to false to call tds.verify() manually. |
endpoint | (production) | PAAY API endpoint. Use https://api-sandbox.3dsintegrator.com/v2.2 for testing. |
verbose | false | Enable detailed console logging for debugging. |
appendForm | true | Automatically inject cavv, eci, and xid as hidden inputs after authentication. |
eciInputId | eci | name attribute for the auto-appended ECI input. |
cavvInputId | cavv | name attribute for the auto-appended CAVV input. |
xidInputId | xid | name attribute for the auto-appended XID input. |
rebill | false | Decimal amount (e.g. 89.95) to also authenticate a rebill. Returns rebill_cavv, rebill_eci, rebill_xid. |
3. Authenticate
Trigger 3DS authentication either automatically as the customer fills in the form, or manually when they click pay.
Auto Submit
With autoSubmit: true (the default), the SDK begins authentication as soon as card number, month, and year are filled. The resulting 3DS values are injected into your form as hidden inputs.
javascript
var tds = new ThreeDS(
'checkout-form',
'YOUR_PAAY_API_KEY',
null,
{
autoSubmit: true,
endpoint: 'https://api-sandbox.3dsintegrator.com/v2.2',
verbose: true
}
);When the customer clicks submit, cavv, eci, and xid are already in the form (assuming authentication completed).
Manual Verify
With autoSubmit: false, call tds.verify() yourself — typically on the pay button click. This lets you inspect the result before submitting.
javascript
var tds = new ThreeDS(
'checkout-form',
'YOUR_PAAY_API_KEY',
null,
{
autoSubmit: false,
endpoint: 'https://api-sandbox.3dsintegrator.com/v2.2',
verbose: true
}
);
document.getElementById('pay-button').addEventListener('click', function(e) {
e.preventDefault();
// Optional: Only authenticate Visa and Mastercard
var cardNumber = document.querySelector('[data-threeds=pan]').value;
var firstDigit = cardNumber.charAt(0);
if (firstDigit === '4' || firstDigit === '5' || firstDigit === '2') {
// Visa (4) or Mastercard (5, 2) — run 3DS authentication
tds.verify({
resolve: function(data) {
console.log('3DS Authentication Result:', data);
console.log('Status:', data.status); // Y, A, N, U, R
console.log('ECI:', data.eci); // e.g., "05"
console.log('CAVV:', data.authenticationValue);
if (data.status === 'Y' || data.status === 'A') {
// Authentication successful — submit payment
submitPayment(data);
} else {
// Authentication failed — decide whether to proceed
alert('3DS authentication was not successful. Status: ' + data.status);
}
},
reject: function(error) {
console.error('3DS Error:', error);
// Decide whether to proceed without 3DS or show error
alert('3DS authentication could not be completed.');
}
});
} else {
// Amex (3), Discover (6), or other — submit without 3DS
submitPayment(null);
}
});4. Response
A successful authentication returns a response object. The fields you need for the gateway API are listed below.
Response Fields
| Field | Description | Use in API |
|---|---|---|
authenticationValue | CAVV — Base64-encoded cryptographic proof of authentication. | Pass as cavv |
eci | Electronic Commerce Indicator. Indicates authentication level (e.g. "05" Visa, "02" Mastercard). | Pass as eci |
status | Authentication status code (see Status Codes). | Decision logic |
dsTransId | Directory Server Transaction ID. Used as the XID for 3DS 2.x. | Pass as xid |
acsTransId | Access Control Server transaction ID. | Optional reference |
protocolVersion | 3DS protocol version (e.g. "2.2.0", "2.1.0"). | Optional reference |
scaIndicator | Whether Strong Customer Authentication (SCA) was applied. | Optional reference |
Example Response
Successful (frictionless):
json
{
"authenticationValue": "XYi1pplo2XITHfJdT21SweFz1us=",
"eci": "05",
"status": "Y",
"protocolVersion": "2.2.0",
"dsTransId": "d65e93c3-35ab-41ba-b307-767bfc19eae3",
"acsTransId": "ca5f9649-b865-47ce-be6f-54422a0fce47",
"scaIndicator": false
}Failed:
json
{
"eci": "07",
"status": "N",
"protocolVersion": "2.2.0",
"dsTransId": "d65e93c3-35ab-41ba-b307-767bfc19eae3",
"acsTransId": "65973509-34be-401c-8534-9712c089a938",
"scaIndicator": false
}Status Codes
| Status | Meaning | Liability Shift | Action |
|---|---|---|---|
Y | Authenticated — Cardholder verified. | Yes | Proceed. Include 3DS data. |
A | Attempted — Issuer doesn't fully support 3DS. | Yes (typically) | Proceed. Include 3DS data. |
N | Not Authenticated — Cardholder failed authentication. | No | Decline, or proceed without 3DS at your own risk. |
R | Rejected — Issuer rejected authentication. | No | Do not proceed. Inform the cardholder. |
U | Unavailable — Could not be performed (technical issue). | No | Proceed without 3DS or retry. |
C | Challenge — Cardholder was challenged (handled by SDK). | Depends | Wait for the final result after challenge. |
5. Submit
Send the card details and 3DS data to /api/transaction from your server.
Frontend
javascript
function submitPayment(threedsData) {
var formData = {
cardNumber: document.querySelector('[name=cardNumber]').value,
cardMonth: document.querySelector('[name=cardMonth]').value,
cardYear: document.querySelector('[name=cardYear]').value,
cvv: document.querySelector('[name=cvv]').value,
amount: 2500 // $25.00 in cents
};
// Include 3DS data if authentication was performed
if (threedsData) {
formData.threeds = {
cavv: threedsData.authenticationValue,
eci: threedsData.eci,
xid: threedsData.dsTransId,
status: threedsData.status,
version: threedsData.protocolVersion
};
}
fetch('/api/charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
.then(function(response) { return response.json(); })
.then(function(data) {
if (data.success) {
window.location.href = '/thank-you';
} else {
alert('Payment failed: ' + data.message);
}
});
}Backend
Your server takes the card data and 3DS values and submits them to the gateway. The 3DS values go in the three_d_secure object on the transaction request.
bash
curl -X POST { https://sandbox.koipay.io }/api/transaction \
-H "Authorization: YOUR_SECRET_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "sale",
"amount": 2500,
"currency": "usd",
"payment_method": {
"card": {
"card_number": "4111111111111111",
"expiration_date": "12/25",
"cvc": "123"
}
},
"three_d_secure": {
"cavv": "XYi1pplo2XITHfJdT21SweFz1us=",
"eci": "05",
"xid": "d65e93c3-35ab-41ba-b307-767bfc19eae3"
}
}'Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | "sale" or "auth" |
amount | integer | Yes | Amount in cents (e.g. 2500 = $25.00) |
currency | string | No | Currency code (default: "usd") |
payment_method.card.card_number | string | Yes | Full card number |
payment_method.card.expiration_date | string | Yes | Card expiration in MM/YY |
payment_method.card.cvc | string | No | Card verification code |
three_d_secure.cavv | string | Yes* | CAVV from PAAY |
three_d_secure.eci | string | Yes* | ECI from PAAY |
three_d_secure.xid | string | Yes* | dsTransId from PAAY |
* Required when submitting a 3DS-authenticated transaction. Omit the entire three_d_secure object if 3DS was not performed.
Note: You can also pass a Tokenizer
tokeninpayment_method.tokeninstead of raw card data. See With Tokenizer.
Full Example
A working HTML example using the PAAY JS SDK with manual verification.
html
<!DOCTYPE html>
<html>
<head>
<title>Checkout with 3DS</title>
<style>
body { font-family: -apple-system, sans-serif; max-width: 480px; margin: 40px auto; }
label { display: block; margin: 12px 0 4px; font-weight: 600; }
input, select { padding: 8px; width: 100%; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; }
.row { display: flex; gap: 12px; }
.row > div { flex: 1; }
.pay-btn { margin-top: 20px; background: #0066cc; color: white; border: none; padding: 14px;
border-radius: 6px; font-size: 16px; cursor: pointer; width: 100%; }
.pay-btn:hover { background: #0052a3; }
.pay-btn:disabled { background: #999; cursor: not-allowed; }
.status { margin-top: 12px; padding: 10px; border-radius: 4px; display: none; }
.status.info { display: block; background: #e8f4fd; color: #0066cc; }
.status.error { display: block; background: #fde8e8; color: #cc0000; }
.status.success { display: block; background: #e8fde8; color: #006600; }
</style>
</head>
<body>
<h1>Checkout — $25.00</h1>
<form id="checkout-form">
<label>Card Number</label>
<input type="text" name="cardNumber" maxlength="19" placeholder="4111 1111 1111 1111"
data-threeds="pan" autocomplete="cc-number" />
<div class="row">
<div>
<label>Month</label>
<select name="cardMonth" data-threeds="month">
<option value="">--</option>
<option value="01">01</option>
<option value="02">02</option>
<option value="03">03</option>
<option value="04">04</option>
<option value="05">05</option>
<option value="06">06</option>
<option value="07">07</option>
<option value="08">08</option>
<option value="09">09</option>
<option value="10">10</option>
<option value="11">11</option>
<option value="12">12</option>
</select>
</div>
<div>
<label>Year</label>
<select name="cardYear" data-threeds="year">
<option value="">--</option>
<option value="25">2025</option>
<option value="26">2026</option>
<option value="27">2027</option>
<option value="28">2028</option>
<option value="29">2029</option>
<option value="30">2030</option>
</select>
</div>
<div>
<label>CVV</label>
<input type="text" name="cvv" maxlength="4" placeholder="123" autocomplete="cc-csc" />
</div>
</div>
<!-- Hidden 3DS fields -->
<input type="hidden" name="x_transaction_id" value="" data-threeds="id" />
<input type="hidden" name="paayAmount" value="25.00" data-threeds="amount" />
<button type="button" class="pay-btn" id="pay-button">Pay $25.00</button>
<div id="status" class="status"></div>
</form>
<!-- PAAY 3DS JS SDK -->
<script src="https://cdn.3dsintegrator.com/threeds.min.latest.js"></script>
<script>
// Generate unique 3DS transaction ID
var idInput = document.querySelector('[data-threeds=id]');
idInput.value = 'id-' + Math.random().toString(36).substr(2, 16);
// Initialize PAAY ThreeDS SDK
var tds = new ThreeDS(
'checkout-form',
'YOUR_PAAY_API_KEY', // Replace with your PAAY API key
null,
{
autoSubmit: false, // Manual control
endpoint: 'https://api-sandbox.3dsintegrator.com/v2.2',
verbose: true
}
);
// Status display helper
function showStatus(message, type) {
var el = document.getElementById('status');
el.textContent = message;
el.className = 'status ' + type;
}
// Handle pay button click
document.getElementById('pay-button').addEventListener('click', function() {
var btn = this;
btn.disabled = true;
btn.textContent = 'Processing...';
showStatus('Authenticating card...', 'info');
var cardNumber = document.querySelector('[name=cardNumber]').value.replace(/\s/g, '');
var firstDigit = cardNumber.charAt(0);
// Only run 3DS for Visa (4) and Mastercard (5, 2)
if (firstDigit === '4' || firstDigit === '5' || firstDigit === '2') {
tds.verify({
resolve: function(data) {
console.log('3DS Result:', data);
if (data.status === 'Y' || data.status === 'A') {
showStatus('Authenticated! Processing payment...', 'success');
submitToServer({
cavv: data.authenticationValue,
eci: data.eci,
xid: data.dsTransId,
status: data.status,
version: data.protocolVersion
});
} else {
showStatus('Authentication was not successful (status: ' + data.status + '). Payment not processed.', 'error');
btn.disabled = false;
btn.textContent = 'Pay $25.00';
}
},
reject: function(error) {
console.error('3DS Error:', error);
showStatus('Authentication error. You may retry or proceed without 3DS.', 'error');
btn.disabled = false;
btn.textContent = 'Pay $25.00';
}
});
} else {
// Non-Visa/MC — proceed without 3DS
showStatus('Processing payment...', 'info');
submitToServer(null);
}
});
function submitToServer(threedsData) {
var payload = {
cardNumber: document.querySelector('[name=cardNumber]').value.replace(/\s/g, ''),
cardMonth: document.querySelector('[name=cardMonth]').value,
cardYear: document.querySelector('[name=cardYear]').value,
cvv: document.querySelector('[name=cvv]').value,
amount: 2500
};
if (threedsData) {
payload.threeds = threedsData;
}
fetch('/api/charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
showStatus('Payment successful!', 'success');
setTimeout(function() { window.location.href = '/thank-you'; }, 1500);
} else {
showStatus('Payment failed: ' + (data.message || 'Unknown error'), 'error');
document.getElementById('pay-button').disabled = false;
document.getElementById('pay-button').textContent = 'Pay $25.00';
}
})
.catch(function() {
showStatus('Network error. Please try again.', 'error');
document.getElementById('pay-button').disabled = false;
document.getElementById('pay-button').textContent = 'Pay $25.00';
});
}
</script>
</body>
</html>Server Example
A Node.js server receiving the frontend payload and calling the gateway API.
javascript
const express = require('express');
const fetch = require('node-fetch');
const app = express();
app.use(express.json());
const GW_API_KEY = 'your_secret_api_key';
const GW_URL = 'https://sandbox.koipay.io/api/transaction';
app.post('/api/charge', async (req, res) => {
const { cardNumber, cardMonth, cardYear, cvv, amount, threeds } = req.body;
// Build the transaction request
const transactionRequest = {
type: 'sale',
amount: amount,
currency: 'usd',
payment_method: {
card: {
card_number: cardNumber,
expiration_date: cardMonth + '/' + cardYear,
cvc: cvv
}
}
};
// Include 3DS data if authentication was performed
if (threeds && threeds.cavv) {
transactionRequest.three_d_secure = {
cavv: threeds.cavv,
eci: threeds.eci,
xid: threeds.xid
};
}
try {
const response = await fetch(GW_URL, {
method: 'POST',
headers: {
'Authorization': GW_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify(transactionRequest)
});
const data = await response.json();
if (data.status === 'success') {
res.json({ success: true, transactionId: data.data.id });
} else {
res.json({ success: false, message: data.msg });
}
} catch (error) {
console.error('Transaction error:', error);
res.json({ success: false, message: 'Internal server error' });
}
});
app.listen(3000, () => console.log('Server running on port 3000'));With Tokenizer
For PCI scope reduction via Tokenizer and manual 3DS control via PAAY, combine both:
- Collect the card via Tokenizer to get a token (set
paay.forceDisabled: trueto skip Tokenizer's built-in 3DS). - Use PAAY's JS SDK on a separate form to authenticate via 3DS.
- Send the token and the 3DS values to your server.
- Submit the transaction with the token as the payment method and the 3DS values pass-through.
json
{
"type": "sale",
"amount": 2500,
"payment_method": {
"token": "the-token-from-tokenizer"
},
"three_d_secure": {
"cavv": "XYi1pplo2XITHfJdT21SweFz1us=",
"eci": "05",
"xid": "d65e93c3-35ab-41ba-b307-767bfc19eae3"
}
}Note: This hybrid approach is advanced. The Tokenizer's built-in 3DS is simpler and recommended for most cases.
ECI Values
The ECI value indicates the authentication level achieved.
| ECI | Visa | Mastercard | Meaning |
|---|---|---|---|
05 | Yes | — | Fully authenticated (Visa) |
06 | Yes | — | Attempted (Visa) |
07 | Yes | — | Not authenticated / non-3DS (Visa) |
02 | — | Yes | Fully authenticated (Mastercard) |
01 | — | Yes | Attempted (Mastercard) |
00 | — | Yes | Not authenticated / non-3DS (Mastercard) |
Testing
PAAY Sandbox
Point the SDK to PAAY's sandbox during development.
javascript
{
endpoint: 'https://api-sandbox.3dsintegrator.com/v2.2',
verbose: true
}Gateway Sandbox
Use your sandbox API key and the sandbox URL for transaction processing.
https://sandbox.koipay.io/api/transactionCombine with our test card numbers and PAAY's sandbox cards to simulate 3DS outcomes.
Production
Update both endpoints when going live:
- PAAY SDK — Remove the
endpointoption or set it to PAAY's production URL. - Gateway API — Switch
https://sandbox.koipay.ioto your production gateway URL. - API keys — Replace sandbox keys with production keys for both PAAY and the gateway.
- Verbose — Set
verbose: falseto stop console logging.
Troubleshooting
| Issue | Solution |
|---|---|
| PAAY SDK not initializing | Verify the form ID matches your <form> element's id attribute. Check the browser console for errors. |
data-threeds inputs not detected | Ensure inputs with data-threeds attributes are inside the form element referenced by the SDK. |
3DS authentication always returns U | Check that the card number is valid and the card issuer supports 3DS. In sandbox, use PAAY's test cards. |
| Challenge window not appearing | Ensure the SDK script is loaded and the form is properly configured. Check verbose: true console output. |
| The gateway rejects 3DS values | Verify that cavv, eci, and xid are passed correctly in the three_d_secure object. Values must be unaltered from the PAAY response. |
tds.verify() not calling resolve/reject | Ensure card number, month, and year fields have valid values before calling verify. |
| Authentication works in sandbox but not production | Confirm you're using production API keys and endpoints for both PAAY and the gateway. |
FAQ
Tokenizer 3DS vs. PAAY SDK directly?
Tokenizer 3DS handles card collection, tokenization, and 3DS in one iframe — simpler. The PAAY SDK approach gives you full control over the form and authentication flow.
Do I need a separate PAAY account?
Yes. PAAY provides the 3DS authentication service. Contact your gateway account rep or PAAY directly.
Does the PAAY SDK see the full card number?
The SDK reads card number, month, and year from your form inputs (tagged with data-threeds) to authenticate with the issuer. Card data goes directly to PAAY — it does not pass through your server during the 3DS step.
Can I authenticate Amex and Discover?
PAAY supports Visa, Mastercard, American Express, and Discover. Authentication rates vary by brand and issuer.
What if 3DS fails — can I still process the transaction?
Yes, you can submit without 3DS data, but you lose the liability shift benefit. Your business rules should decide whether to proceed.
Can I use this for recurring payments?
Yes. Authenticate the initial transaction with 3DS, then use those credentials for subsequent charges. PAAY's rebill option can authenticate the rebill amount separately. See the Recurring API docs.