Skip to content

3D Secure (3DS) with Tokenizer

Tokenizer has built-in support for 3D Secure authentication via PAAY. When 3DS is enabled on your account, Tokenizer handles the entire authentication flow automatically — no additional scripts or redirects required.

What is 3D Secure?

3D Secure (3DS) is a security protocol that adds an extra layer of authentication for card-not-present transactions. It helps verify that the person making a purchase is the actual cardholder, reducing fraud and shifting chargeback liability from the merchant to the card issuer.

With our Tokenizer, 3DS authentication happens seamlessly in the background. In most cases, the cardholder never sees a challenge — the authentication completes via a "frictionless" flow. When a challenge is required (e.g., entering a one-time code), the Tokenizer handles displaying the challenge UI automatically.

Benefits

  • Liability shift — Fraudulent chargeback liability shifts from the merchant to the card issuer for authenticated transactions
  • Reduced fraud — Verifies cardholder identity before the transaction is processed
  • No extra integration work — Tokenizer manages the entire 3DS flow within the existing iframe
  • Automatic handling — Frictionless authentication happens without any customer interaction

Prerequisites

Before using 3DS with Tokenizer, ensure the following:

  1. 3DS is enabled on your account — Contact your gateway account representative or ISO/Agent to enable 3DS (PAAY) on your merchant account.
  2. Public API key — You'll need your pub_XXXX key from the Control Panel (Settings → API Keys).
  3. Tokenizer script loaded — Include the Tokenizer JavaScript library on your page.

TIP

TIP: Make sure to replace https://sandbox.koipay.io with Also make sure to replace ENV_https://sandbox.koipay.io with for sandbox and for production.


Basic 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: You must pass the transaction amount as a string to tokenizer.submit() when using 3DS. See Submitting with Amount for details.


Configuration Options

The paay settings object accepts the following options:

OptionTypeDefaultDescription
sandboxbooleanfalseSet to true to use the PAAY sandbox environment for testing. Use this during development and QA.
forceDisabledbooleanfalseSet to true to skip 3DS authentication entirely. Useful for testing non-3DS flows or temporarily disabling 3DS.
rejectChallengesarray[]An array of 3DS authentication statuses that should be rejected. Transactions with these statuses will not return a token.

Example: Testing in Sandbox

javascript
settings: {
  paay: {
    sandbox: true  // Use PAAY sandbox for testing
  }
}

Example: Disabling 3DS

javascript
settings: {
  paay: {
    forceDisabled: true  // Skip 3DS authentication
  }
}

Example: Rejecting Specific Statuses

javascript
settings: {
  paay: {
    rejectChallenges: ['N', 'R', 'U']  // Reject failed, rejected, and unavailable authentications
  }
}

Submitting with Amount

When 3DS is enabled, you must pass the transaction amount as a string to tokenizer.submit(). This is required because the amount is part of the 3DS authentication request sent to the card 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): If you call tokenizer.submit() without an amount while 3DS is enabled, the authentication will run as a Non-Payment Authentication. NPA is used for verifying cards without charging them (e.g., adding a card to a vault). If your intent is a payment transaction, always include the amount.


Handling the 3DS Response

When 3DS is enabled, the submission callback's response object includes a paay property containing 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
      })
    })
  }
}

3DS Response Fields

The resp.paay object may contain the following fields depending on the authentication outcome:

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

3DS Status Codes

StatusMeaningLiability Shift
YAuthenticated — Cardholder successfully authenticated.✅ Yes
AAttempted — Authentication was attempted but the card issuer does not support it, or the cardholder is not enrolled.✅ Yes (in most cases)
NNot Authenticated — Cardholder failed authentication.❌ No
RRejected — Authentication was rejected by the issuer.❌ No
UUnavailable — Authentication could not be performed (technical issue).❌ No
CChallenge Required — A challenge was presented to the cardholder. The final result depends on the challenge outcome.Depends on outcome

How the 3DS Flow Works

Understanding the behind-the-scenes flow can help with debugging and integration decisions:

  1. Customer enters card details — Card data is entered into the secure Tokenizer iframe as usual.
  2. You call tokenizer.submit('amount') — Tokenizer sends the card data and amount to the PAAY 3DS service.
  3. Frictionless check — PAAY communicates with the card issuer's Access Control Server (ACS) to determine if the transaction can be authenticated without customer interaction.
  4. Challenge (if needed) — If the issuer requires additional verification, a challenge window is automatically displayed within the Tokenizer iframe (e.g., a one-time passcode entry).
  5. Authentication result — The 3DS authentication result (CAVV, ECI, XID) is returned alongside the payment token in your submission callback.
  6. Your server processes the transaction — Send the token and 3DS data to your backend, which calls the API to complete the charge.
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 Processing

After receiving the token and 3DS data in your frontend callback, send them to your server. Your server then makes the API call to the API with the 3DS authentication values included.

Example: Processing a 3DS-Authenticated Transaction

Frontend → Your 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)
    }
  })
}

Your Server → Our API:

json
{
  "type": "sale",
  "amount": 2500,
  "payment_method": {
    "token": "the-token-from-tokenizer"
  }
}

The 3DS authentication data (CAVV, ECI, XID) is associated with the token automatically when 3DS is processed through the Tokenizer. Your API call uses the token as normal — the gateway links the 3DS results to the transaction.

See Transaction Processing for complete API documentation.


Complete Example

Here's a full 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>

Rejecting Challenges

The rejectChallenges option lets you control which 3DS authentication outcomes are acceptable. If the authentication returns a status listed in rejectChallenges, the Tokenizer will not return a token, and the submission callback will fire with an error status instead.

This is useful when you want to enforce strict authentication requirements, for example:

javascript
settings: {
  paay: {
    // Only accept fully authenticated (Y) or attempted (A) transactions
    rejectChallenges: ['N', 'R', 'U']
  }
}
Use CaserejectChallenges
Accept all outcomes (most permissive)[]
Reject only failed authentications['N']
Reject failed and rejected['N', 'R']
Strict: reject anything that isn't fully authenticated['N', 'R', 'U']

For a complete list of statuses, see the PAAY 3DS Response Table.


Testing

Sandbox Mode

During development, enable sandbox mode to test 3DS flows without real card network authentication:

javascript
settings: {
  paay: {
    sandbox: true
  }
}

Use sandbox mode alongside the our test card numbers to simulate various 3DS outcomes.

Disabling 3DS for Testing

To test your payment flow without 3DS (while keeping 3DS enabled on your account), use forceDisabled:

javascript
settings: {
  paay: {
    forceDisabled: true
  }
}

Remember: Remove forceDisabled: true and set sandbox: false before going to production.


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 is processed through the 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?

In most cases, no. The majority of 3DS authentications complete via a "frictionless" flow where the cardholder sees nothing different. Only when the card issuer requires additional verification (a "challenge") will the customer need to take action, such as entering a one-time code.

Can I use 3DS with ACH payments?

No. 3DS is specific to card-based transactions. It does not apply to ACH/bank account payments.

What happens if 3DS authentication fails?

It depends on your rejectChallenges configuration. If the failed status is in your rejection list, no token is returned. If not, you'll receive the token along with the 3DS results, and you can decide server-side whether to proceed.

Is 3DS required?

3DS is not required for all transactions, but it is strongly recommended for card-not-present payments. In some regions (such as the EU under PSD2/SCA regulations), 3DS or equivalent strong customer authentication is mandatory. 3DS can also reduce chargebacks and shift fraud liability away from the merchant.

What card brands support 3DS?

Visa, Mastercard, American Express, and Discover all support 3DS authentication through PAAY. Support for additional networks may be available — contact PAAY for details.