Appearance
3DS Tokenizer
Tokenizer has built-in 3D Secure authentication via PAAY. When 3DS is enabled on your account, Tokenizer handles the full flow inside its iframe — no extra scripts or redirects.
Overview
3D Secure (3DS) adds an extra layer of authentication for card-not-present transactions. It verifies the cardholder, reduces fraud, and shifts chargeback liability from the merchant to the issuer.
With Tokenizer, most authentications complete via a "frictionless" flow with no customer interaction. When a challenge is required (e.g. a one-time code), Tokenizer renders the challenge UI automatically.
Benefits
- Liability shift — Chargeback liability shifts to the issuer for authenticated transactions.
- Reduced fraud — Verifies cardholder identity before processing.
- No extra work — 3DS runs inside the existing Tokenizer iframe.
- Automatic — Frictionless authentication happens with no customer interaction.
Prerequisites
Before using 3DS with Tokenizer, you need:
- 3DS enabled on your account — Contact your gateway rep or ISO/Agent to enable 3DS (PAAY).
- Public API key — Your
pub_XXXXkey from the Control Panel (Settings → API Keys). - Tokenizer script — Include the Tokenizer JS library on your page.
TIP
Replace https://sandbox.koipay.io with . Replace ENV_https://sandbox.koipay.io with for sandbox and for production.
Setup
Enable 3DS by adding the paay settings object to your Tokenizer configuration.
html
<!DOCTYPE html>
<html>
<head>
<title>Checkout with 3DS</title>
<script src="https://sandbox.koipay.io/tokenizer/tokenizer.js"></script>
</head>
<body>
<h1>Checkout</h1>
<div id="payment-form"></div>
<button onclick="tokenizer.submit('25.00')">Pay $25.00</button>
<script>
var tokenizer = new Tokenizer({
url: 'https://sandbox.koipay.io',
apikey: 'pub_XXXXXXXXXXXXXX',
container: '#payment-form',
submission: function(resp) {
if (resp.status === 'success') {
console.log('Token:', resp.token)
console.log('3DS Results:', resp.paay)
sendTokenToServer(resp.token, resp.paay)
}
},
settings: {
paay: {
sandbox: false,
forceDisabled: false,
rejectChallenges: []
}
}
})
</script>
</body>
</html>Important: Pass the transaction amount as a string to
tokenizer.submit()when using 3DS. See Amount.
Options
The paay settings object accepts these options.
| Option | Type | Default | Description |
|---|---|---|---|
sandbox | boolean | false | Use the PAAY sandbox environment for testing. |
forceDisabled | boolean | false | Skip 3DS authentication entirely. Useful for testing non-3DS flows. |
rejectChallenges | array | [] | 3DS statuses to reject — transactions with these statuses won't return a token. |
javascript
settings: {
paay: {
sandbox: true,
forceDisabled: false,
rejectChallenges: ['N', 'R', 'U']
}
}Amount
When 3DS is enabled, you must pass the transaction amount as a string to tokenizer.submit(). The amount is part of the 3DS authentication request sent to the issuer.
javascript
// ✅ Correct — amount passed as a string
tokenizer.submit('25.00')
// ✅ Correct — dynamic amount
var amount = document.getElementById('order-total').textContent
tokenizer.submit(amount)
// ❌ Incorrect — no amount passed
tokenizer.submit()Non-Payment Authentication (NPA): Calling
tokenizer.submit()without an amount runs as NPA — used for verifying cards without charging them (e.g. adding to a vault). For payment transactions, always include the amount.
Response
The submission callback's response includes a paay property with the 3DS authentication results.
javascript
submission: function(resp) {
if (resp.status === 'success') {
console.log('Token:', resp.token)
// 3DS authentication results
console.log('3DS Data:', resp.paay)
// Send both token and 3DS data to your server
fetch('/api/charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: resp.token,
amount: 2500, // Amount in cents
paay: resp.paay // Pass 3DS results to your backend
})
})
}
}Response Fields
The resp.paay object may include these fields depending on the outcome.
| Field | Description |
|---|---|
cavv | Cardholder Authentication Verification Value — cryptographic proof the authentication occurred. |
eci | Electronic Commerce Indicator — outcome/level of authentication. |
xid | Transaction identifier for the 3DS authentication. |
status | Authentication status code (see Status Codes). |
Status Codes
| Status | Meaning | Liability Shift |
|---|---|---|
Y | Authenticated — Cardholder verified. | Yes |
A | Attempted — Issuer doesn't support 3DS or cardholder not enrolled. | Yes (typically) |
N | Not Authenticated — Cardholder failed authentication. | No |
R | Rejected — Issuer rejected authentication. | No |
U | Unavailable — Could not be performed (technical issue). | No |
C | Challenge — Cardholder was challenged. Final result depends on outcome. | Depends |
Flow
The end-to-end 3DS flow when using Tokenizer:
- Customer enters card — Card data is entered into the secure Tokenizer iframe.
- You call
tokenizer.submit('amount')— Tokenizer sends the card and amount to the PAAY 3DS service. - Frictionless check — PAAY contacts the issuer's Access Control Server (ACS) to see if the transaction can authenticate without customer interaction.
- Challenge (if needed) — If the issuer requires verification, the challenge UI is rendered inside the Tokenizer iframe.
- Result — CAVV, ECI, and XID are returned alongside the payment token in your
submissioncallback. - Your server charges it — Send the token to your backend, which calls the gateway API.
Customer → Tokenizer iframe → PAAY 3DS → Card Issuer (ACS)
↓
Frictionless? ─── Yes ──→ Auth Result + Token → Your Callback
│
No
↓
Challenge UI ──→ Customer Completes ──→ Auth Result + Token → Your CallbackServer Side
Send the token and 3DS data from your frontend callback to your server, then call the gateway API.
Frontend → Server
javascript
function sendTokenToServer(token, paayData) {
fetch('/api/charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: token,
amount: 2500,
paay: paayData
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.href = '/thank-you'
} else {
alert('Payment failed: ' + data.message)
}
})
}Server → Gateway
json
{
"type": "sale",
"amount": 2500,
"payment_method": {
"token": "the-token-from-tokenizer"
}
}The 3DS data (CAVV, ECI, XID) is automatically linked to the token when 3DS is processed via Tokenizer. Your API call uses the token as normal — the gateway associates the 3DS results to the transaction.
See Transaction Processing for full API docs.
Full Example
A working example with 3DS enabled, fee calculation, and error handling.
html
<!DOCTYPE html>
<html>
<head>
<title>Secure Checkout</title>
<script src="https://sandbox.koipay.io/tokenizer/tokenizer.js"></script>
<style>
body { font-family: -apple-system, sans-serif; max-width: 500px; margin: 40px auto; }
#payment-form { margin: 20px 0; }
.pay-btn { background: #0066cc; color: white; border: none; padding: 12px 24px;
border-radius: 6px; font-size: 16px; cursor: pointer; width: 100%; }
.pay-btn:hover { background: #0052a3; }
.error { color: #cc0000; margin-top: 10px; }
.fee-info { color: #666; font-size: 14px; margin: 10px 0; }
</style>
</head>
<body>
<h1>Checkout</h1>
<p>Order Total: <strong>$25.00</strong></p>
<div id="fee-display" class="fee-info"></div>
<div id="payment-form"></div>
<button class="pay-btn" onclick="handlePayment()">Pay Now</button>
<div id="error-message" class="error"></div>
<script>
var orderAmount = '25.00'
var orderAmountCents = 2500
var tokenizer = new Tokenizer({
url: 'https://sandbox.koipay.io',
apikey: 'pub_XXXXXXXXXXXXXX',
container: '#payment-form',
amount: orderAmountCents,
// 3DS configuration
settings: {
paay: {
sandbox: true, // Set to false for production
forceDisabled: false,
rejectChallenges: ['N', 'R'] // Reject failed and rejected authentications
},
payment: {
calculateFees: true // Optional: calculate surcharges
}
},
// Called when a valid card is entered
validCard: function(card) {
var feeDisplay = document.getElementById('fee-display')
if (card.ServiceFee > 0) {
feeDisplay.textContent = 'Service fee: $' + (card.ServiceFee / 100).toFixed(2)
}
if (card.Disclosure) {
feeDisplay.textContent += ' — ' + card.Disclosure
}
},
// Called after submit() completes
submission: function(resp) {
var errorEl = document.getElementById('error-message')
errorEl.textContent = ''
switch (resp.status) {
case 'success':
// Token and 3DS results received
console.log('Token:', resp.token)
console.log('3DS Authentication:', resp.paay)
sendTokenToServer(resp.token, resp.paay)
break
case 'validation':
// Form fields invalid
errorEl.textContent = 'Please check your payment details: ' +
resp.invalid.join(', ')
break
default:
// Error occurred
errorEl.textContent = resp.msg || 'An error occurred. Please try again.'
}
}
})
function handlePayment() {
// Pass amount for 3DS authentication
tokenizer.submit(orderAmount)
}
function sendTokenToServer(token, paayData) {
fetch('/api/charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: token,
amount: orderAmountCents,
paay: paayData
})
})
.then(function(response) { return response.json() })
.then(function(data) {
if (data.success) {
window.location.href = '/thank-you'
} else {
document.getElementById('error-message').textContent =
'Payment failed: ' + data.message
}
})
.catch(function() {
document.getElementById('error-message').textContent =
'Network error. Please try again.'
})
}
</script>
</body>
</html>Reject Challenges
rejectChallenges controls which 3DS outcomes are acceptable. If a returned status is in the list, Tokenizer will not return a token — the submission callback fires with an error instead.
javascript
settings: {
paay: {
// Only accept fully authenticated (Y) or attempted (A)
rejectChallenges: ['N', 'R', 'U']
}
}| Use Case | rejectChallenges |
|---|---|
| Accept all outcomes | [] |
| Reject failed only | ['N'] |
| Reject failed and rejected | ['N', 'R'] |
Strict — only Y or A | ['N', 'R', 'U'] |
For the full list of statuses, see PAAY's 3DS Response Table.
Testing
Sandbox
Enable sandbox mode during development to test 3DS without real card network authentication.
javascript
settings: {
paay: {
sandbox: true
}
}Combine with our test cards to simulate 3DS outcomes.
Disable 3DS
Use forceDisabled to test your payment flow without 3DS while keeping 3DS enabled on your account.
javascript
settings: {
paay: {
forceDisabled: true
}
}Remember: Remove
forceDisabled: trueand setsandbox: falsebefore going live.
Troubleshooting
| Issue | Solution |
|---|---|
| 3DS not triggering | Verify 3DS (PAAY) is enabled on your merchant account. Contact support. |
| NPA instead of payment auth | Make sure you're passing the amount to tokenizer.submit('25.00'). Without an amount, 3DS runs as Non-Payment Authentication. |
| Challenge not appearing | Challenges are displayed by the card issuer. In sandbox/test mode, challenges may not trigger. Use test cards that simulate challenge flows. |
| Token not returned after 3DS | Check your rejectChallenges array — the authentication status may be in your rejection list. |
| 3DS data missing in callback | Ensure 3DS is enabled on your account and forceDisabled is not set to true. Check resp.paay in the submission callback. |
| Timeout during authentication | 3DS authentication involves communication with the card issuer's servers. Network latency or issuer downtime can cause timeouts. Retry the transaction. |
resp.status is error after 3DS | Check resp.msg for details. Common causes include invalid API key, expired token, or network issues with the PAAY service. |
FAQ
Do I need to change my server-side API calls for 3DS?
No. When 3DS runs through Tokenizer, the authentication data is linked to the token. Your server uses the token in the standard transaction API call.
Does 3DS affect the customer experience?
Usually not. Most authentications complete frictionlessly — the cardholder sees nothing different. Only when the issuer requires a challenge will the customer need to take action (e.g. enter a one-time code).
Can I use 3DS with ACH?
No. 3DS is for card-based transactions only.
What happens if 3DS fails?
It depends on rejectChallenges. If the failed status is in the list, no token is returned. Otherwise you receive the token plus the 3DS results and can decide server-side whether to proceed.
Is 3DS required?
Not for all transactions, but strongly recommended for card-not-present. In some regions (e.g. EU under PSD2/SCA), 3DS or equivalent strong customer authentication is mandatory.
What card brands support 3DS?
Visa, Mastercard, American Express, and Discover all support 3DS through PAAY.