Skip to content

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:

  1. 3DS enabled on your account — Contact your gateway rep or ISO/Agent to enable 3DS (PAAY).
  2. Public API key — Your pub_XXXX key from the Control Panel (Settings → API Keys).
  3. 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.

OptionTypeDefaultDescription
sandboxbooleanfalseUse the PAAY sandbox environment for testing.
forceDisabledbooleanfalseSkip 3DS authentication entirely. Useful for testing non-3DS flows.
rejectChallengesarray[]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.

FieldDescription
cavvCardholder Authentication Verification Value — cryptographic proof the authentication occurred.
eciElectronic Commerce Indicator — outcome/level of authentication.
xidTransaction identifier for the 3DS authentication.
statusAuthentication status code (see Status Codes).

Status Codes

StatusMeaningLiability Shift
YAuthenticated — Cardholder verified.Yes
AAttempted — Issuer doesn't support 3DS or cardholder not enrolled.Yes (typically)
NNot Authenticated — Cardholder failed authentication.No
RRejected — Issuer rejected authentication.No
UUnavailable — Could not be performed (technical issue).No
CChallenge — Cardholder was challenged. Final result depends on outcome.Depends

Flow

The end-to-end 3DS flow when using Tokenizer:

  1. Customer enters card — Card data is entered into the secure Tokenizer iframe.
  2. You call tokenizer.submit('amount') — Tokenizer sends the card and amount to the PAAY 3DS service.
  3. Frictionless check — PAAY contacts the issuer's Access Control Server (ACS) to see if the transaction can authenticate without customer interaction.
  4. Challenge (if needed) — If the issuer requires verification, the challenge UI is rendered inside the Tokenizer iframe.
  5. Result — CAVV, ECI, and XID are returned alongside the payment token in your submission callback.
  6. 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 Callback

Server 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 CaserejectChallenges
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: true and set sandbox: false before going live.


Troubleshooting

IssueSolution
3DS not triggeringVerify 3DS (PAAY) is enabled on your merchant account. Contact support.
NPA instead of payment authMake sure you're passing the amount to tokenizer.submit('25.00'). Without an amount, 3DS runs as Non-Payment Authentication.
Challenge not appearingChallenges 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 3DSCheck your rejectChallenges array — the authentication status may be in your rejection list.
3DS data missing in callbackEnsure 3DS is enabled on your account and forceDisabled is not set to true. Check resp.paay in the submission callback.
Timeout during authentication3DS 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 3DSCheck 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.