Skip to content

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

ApproachBest For
Tokenizer 3DSStandard 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:

  1. Authenticate — PAAY's JS SDK authenticates the cardholder with the issuer on your checkout page.
  2. Collect — PAAY returns the 3DS values (CAVV, ECI, XID/dsTransId) to your frontend.
  3. 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:

  1. PAAY account — Active account with PAAY (3DS Integrator). Contact your account rep or info@paay.co.
  2. PAAY API key — From the PAAY Portal. Separate from your Gateway API key.
  3. Gateway API key — Your secret key (api_XXXX) for server-side calls.
  4. 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

AttributeInputDescription
data-threeds="pan"Card numberFull credit card number.
data-threeds="month"Expiration month1 or 2 digit month (e.g. 01, 12).
data-threeds="year"Expiration year2 digit year (e.g. 25, 30).
data-threeds="id"Transaction IDUnique identifier for this 3DS session. Auto-generated below.
data-threeds="amount"Transaction amountDecimal 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

ParameterDescription
formIdThe id of the <form> element with your card inputs.
apiKeyYour PAAY API key from the PAAY Portal.
jwtPass null to let the SDK generate one, or provide your own server-generated JWT.
optionsConfiguration object — see below.

SDK Options

OptionDefaultDescription
autoSubmittrueAuto-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.
verbosefalseEnable detailed console logging for debugging.
appendFormtrueAutomatically inject cavv, eci, and xid as hidden inputs after authentication.
eciInputIdeciname attribute for the auto-appended ECI input.
cavvInputIdcavvname attribute for the auto-appended CAVV input.
xidInputIdxidname attribute for the auto-appended XID input.
rebillfalseDecimal 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

FieldDescriptionUse in API
authenticationValueCAVV — Base64-encoded cryptographic proof of authentication.Pass as cavv
eciElectronic Commerce Indicator. Indicates authentication level (e.g. "05" Visa, "02" Mastercard).Pass as eci
statusAuthentication status code (see Status Codes).Decision logic
dsTransIdDirectory Server Transaction ID. Used as the XID for 3DS 2.x.Pass as xid
acsTransIdAccess Control Server transaction ID.Optional reference
protocolVersion3DS protocol version (e.g. "2.2.0", "2.1.0").Optional reference
scaIndicatorWhether 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

StatusMeaningLiability ShiftAction
YAuthenticated — Cardholder verified.YesProceed. Include 3DS data.
AAttempted — Issuer doesn't fully support 3DS.Yes (typically)Proceed. Include 3DS data.
NNot Authenticated — Cardholder failed authentication.NoDecline, or proceed without 3DS at your own risk.
RRejected — Issuer rejected authentication.NoDo not proceed. Inform the cardholder.
UUnavailable — Could not be performed (technical issue).NoProceed without 3DS or retry.
CChallenge — Cardholder was challenged (handled by SDK).DependsWait 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

FieldTypeRequiredDescription
typestringYes"sale" or "auth"
amountintegerYesAmount in cents (e.g. 2500 = $25.00)
currencystringNoCurrency code (default: "usd")
payment_method.card.card_numberstringYesFull card number
payment_method.card.expiration_datestringYesCard expiration in MM/YY
payment_method.card.cvcstringNoCard verification code
three_d_secure.cavvstringYes*CAVV from PAAY
three_d_secure.ecistringYes*ECI from PAAY
three_d_secure.xidstringYes*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 token in payment_method.token instead 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:

  1. Collect the card via Tokenizer to get a token (set paay.forceDisabled: true to skip Tokenizer's built-in 3DS).
  2. Use PAAY's JS SDK on a separate form to authenticate via 3DS.
  3. Send the token and the 3DS values to your server.
  4. 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.

ECIVisaMastercardMeaning
05YesFully authenticated (Visa)
06YesAttempted (Visa)
07YesNot authenticated / non-3DS (Visa)
02YesFully authenticated (Mastercard)
01YesAttempted (Mastercard)
00YesNot 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/transaction

Combine with our test card numbers and PAAY's sandbox cards to simulate 3DS outcomes.

Production

Update both endpoints when going live:

  1. PAAY SDK — Remove the endpoint option or set it to PAAY's production URL.
  2. Gateway API — Switch https://sandbox.koipay.io to your production gateway URL.
  3. API keys — Replace sandbox keys with production keys for both PAAY and the gateway.
  4. Verbose — Set verbose: false to stop console logging.

Troubleshooting

IssueSolution
PAAY SDK not initializingVerify the form ID matches your <form> element's id attribute. Check the browser console for errors.
data-threeds inputs not detectedEnsure inputs with data-threeds attributes are inside the form element referenced by the SDK.
3DS authentication always returns UCheck that the card number is valid and the card issuer supports 3DS. In sandbox, use PAAY's test cards.
Challenge window not appearingEnsure the SDK script is loaded and the form is properly configured. Check verbose: true console output.
The gateway rejects 3DS valuesVerify 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/rejectEnsure card number, month, and year fields have valid values before calling verify.
Authentication works in sandbox but not productionConfirm 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.