Inyo

Tokenizing Cards

Overview

PCI DSS requirements prohibit storing or transmitting raw card data unless you hold the appropriate certification. Inyo's tokenization solution handles this for you: the inyo.js library encrypts card data directly in the browser and returns a token that only your API keys can use.

How it works:

  1. Load inyo.js in your payment page
  2. Add data-field attributes to your card input fields
  3. Initialize InyoTokenizer with your public key and callbacks
  4. Call tokenizeCard() when the user submits — the library reads the form, encrypts the data, and returns a token
  5. Send the token to your backend to create a payment via POST /v2/payment

Domain Whitelisting (Required)

For CORS and security reasons, the URL of the page that loads the tokenizer must be registered with Inyo before it can make tokenization requests. This applies to every environment (sandbox and production).

  • Before going live, provide Inyo with the full origin URL (e.g., https://checkout.yoursite.com) of every page that will call the tokenizer.
  • Any change to the URL (new domain, subdomain, or port) must be communicated to Inyo so the whitelist can be updated.
  • Tokenization requests from non-whitelisted origins will be blocked by CORS.

If you are using the PostMessage integration for 3DS, the URL of the page receiving the postMessage events must also be registered with Inyo. This ensures that only your intended page can listen to 3DS authentication results. See Handling 3D Secure for details.

Contact your Inyo integration manager or [email protected] to register your URLs.

Loading the Library

Include the tokenizer script with an integrity hash to prevent tampering:

<script
  src="https://cdn.simpleps.com/sandbox/inyo.js"
  integrity="sha384-..."
  crossorigin="anonymous">
</script>
EnvironmentURL
Sandboxhttps://cdn.simpleps.com/sandbox/inyo.js
Productionhttps://cdn.simpleps.com/production/inyo.js

HTML Form Setup

Add data-field attributes to your card input elements. The tokenizer binds to these automatically — no need to read input values yourself.

<div id="payment-form">
  <!-- Cardholder Name -->
  <input type="text" data-field="cardholder" id="cc-name" required>

  <!-- Card Number (PAN) -->
  <input type="text" data-field="pan" id="cc-number" maxlength="19" required>

  <!-- Expiration Date (MM/YY) -->
  <input type="text" data-field="expirationDate" id="cc-expiration"
         maxlength="5" placeholder="MM/YY" required>

  <!-- CVV / Security Code -->
  <input type="text" data-field="securitycode" id="cc-cvv"
         maxlength="4" required>

  <button type="button" id="pay-btn">Pay Now</button>
</div>

Required data-field values:

data-fieldInputNotes
cardholderFull name on cardAs printed on the card
panCard number13–19 digits
expirationDateExpiryFormat: MM/YY
securitycodeCVV/CVC3 digits (Visa/MC/Discover) or 4 digits (Amex)

Initializing the Tokenizer

Create an InyoTokenizer instance when the DOM is ready. The 3DS configuration depends on which integration method you choose — URL Redirect or PostMessage:

Example — URL Redirect (traditional)

The 3DS provider redirects the browser to your successUrl or failUrl after authentication. Best for server-rendered apps and full-page checkout flows.

document.addEventListener('DOMContentLoaded', () => {
  const tokenizer = new InyoTokenizer({
    targetId: '#payment-form',
    publicKey: 'YOUR_PUBLIC_KEY',
    storeLaterUse: false,
    threeDSData: {
      enable: true,
      successUrl: 'https://yoursite.com/3ds/success',
      failUrl: 'https://yoursite.com/3ds/fail'
    },
    successCallback: handleSuccess,
    errorCallback: handleError
  });

  document.getElementById('pay-btn').addEventListener('click', () => {
    tokenizer.tokenizeCard();
  });
});

Example — PostMessage (SPA / embedded)

The 3DS result is delivered via the browser's postMessage API to the parent window. Best for single-page apps and iframe/modal checkout experiences — no page navigation required.

document.addEventListener('DOMContentLoaded', () => {
  const tokenizer = new InyoTokenizer({
    targetId: '#payment-form',
    publicKey: 'YOUR_PUBLIC_KEY',
    storeLaterUse: false,
    threeDSData: {
      enable: true,
      enablePostMessage: true
    },
    successCallback: handleSuccess,
    errorCallback: handleError
  });

  document.getElementById('pay-btn').addEventListener('click', () => {
    tokenizer.tokenizeCard();
  });
});

When using enablePostMessage: true, you do not provide successUrl or failUrl. Instead, you listen for message events on the parent window after opening the redirectAcsUrl in an iframe. See Handling 3D Secure for the complete implementation guide.

Configuration Parameters

ParameterTypeRequiredDescription
targetIdstringCSS selector of the container holding the data-field inputs (e.g., '#payment-form')
publicKeystringYour merchant public key, provided by Inyo
successCallbackfunctionCalled when tokenization succeeds
errorCallbackfunctionCalled when tokenization fails
storeLaterUsebooleanfalse (default) = one-time token; true = recurring/stored token
threeDSDataobject3D Secure configuration (see below)

3DS Configuration (threeDSData)

FieldTypeRequiredDescription
enablebooleanWhether to request 3DS authentication
successUrlstring⚠️Redirect mode only. URL where the 3DS provider redirects on success
failUrlstring⚠️Redirect mode only. URL where the 3DS provider redirects on failure
enablePostMessageboolean⚠️PostMessage mode only. Set true to receive 3DS results via window.postMessage instead of URL redirect

Choose one mode:

  • Redirect: Set successUrl + failUrl. Do not set enablePostMessage.
  • PostMessage: Set enablePostMessage: true. Do not set successUrl / failUrl.

Enabling 3DS does not guarantee a challenge will occur — the issuing bank decides based on its risk assessment. See Handling 3D Secure for the full flow and code examples for both modes.

Handling Callbacks

Success Callback

function handleSuccess(response) {
  console.log('Tokenization response:', response);

  if (response.reasonCode === 'WAITING_TRANSACTION') {
    // Token created — extract it
    const token = response.additionalData.token;
    const lastFour = response.additionalData.lastFour;

    console.log(`Card ending in ${lastFour}, token: ${token}`);

    // Send the token to your backend server
    // Your backend will call POST /v2/payment with this token
    submitPaymentToBackend(token);
  } else {
    console.warn('Unexpected response step:', response.step);
  }
}

Success response fields:

FieldDescription
reasonCode"WAITING_TRANSACTION" when token is ready
additionalData.tokenThe card token UUID — use as cardTokenId in payment requests
additionalData.lastFourLast 4 digits of the card number (for display)

Error Callback

function handleError(response) {
  console.error('Tokenization error:', response);

  // Clear previous error states
  document.querySelectorAll('#cc-number, #cc-expiration, #cc-cvv')
    .forEach(el => el.classList.remove('is-invalid'));

  // Highlight the specific field that failed
  switch (response.code) {
    case 'INVALID_PAN':
      document.querySelector('#cc-number').classList.add('is-invalid');
      break;
    case 'INVALID_EXPIRY_DATE':
      document.querySelector('#cc-expiration').classList.add('is-invalid');
      break;
    case 'INVALID_CVV':
      document.querySelector('#cc-cvv').classList.add('is-invalid');
      break;
    default:
      console.error('Unknown error code:', response.code);
  }
}

Error codes:

CodeDescription
INVALID_PANCard number is invalid (failed Luhn check or unsupported scheme)
INVALID_EXPIRY_DATEExpiration date is invalid or card is expired
INVALID_CVVSecurity code is invalid

One-Time vs. Recurring Tokens

Token TypestoreLaterUseUsage
One-timefalseSingle transaction only — cannot be reused
RecurringtrueCan be stored and reused for future charges

Rules:

  • You must not store one-time tokens after use
  • You may store recurring tokens server-side for future transactions
  • You must never store raw card data (PAN, CVV, expiration) regardless of token type
  • When using a stored recurring token for a subsequent payment, pass the previousPaymentId (from the original authorization) alongside the cardTokenId

Complete Examples

Example A — URL Redirect (server-rendered checkout)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Inyo Payment — Redirect</title>
</head>
<body>

  <form id="checkout-form" novalidate>
    <div id="payment-form">
      <div>
        <label for="cc-name">Name on Card</label>
        <input type="text" data-field="cardholder" id="cc-name" required>
      </div>
      <div>
        <label for="cc-number">Card Number</label>
        <input type="text" data-field="pan" id="cc-number" maxlength="19" required>
      </div>
      <div>
        <label for="cc-expiration">Expiration</label>
        <input type="text" data-field="expirationDate" id="cc-expiration"
               placeholder="MM/YY" maxlength="5" required>
      </div>
      <div>
        <label for="cc-cvv">CVV</label>
        <input type="text" data-field="securitycode" id="cc-cvv"
               maxlength="4" required>
      </div>
      <button type="button" id="pay-btn">Pay $99.99</button>
    </div>
  </form>

  <script src="https://cdn.simpleps.com/sandbox/inyo.js"></script>
  <script>
    document.addEventListener('DOMContentLoaded', () => {
      const tokenizer = new InyoTokenizer({
        targetId: '#payment-form',
        publicKey: 'YOUR_PUBLIC_KEY',
        storeLaterUse: false,
        threeDSData: {
          enable: true,
          successUrl: 'https://yoursite.com/3ds/success',
          failUrl: 'https://yoursite.com/3ds/fail'
        },
        successCallback: handleSuccess,
        errorCallback: handleError
      });

      document.getElementById('pay-btn').addEventListener('click', () => {
        const form = document.getElementById('checkout-form');
        if (form.checkValidity()) {
          tokenizer.tokenizeCard();
        } else {
          form.classList.add('was-validated');
        }
      });
    });

    function handleSuccess(response) {
      if (response.reasonCode === 'WAITING_TRANSACTION') {
        const token = response.additionalData.token;
        // Send token to your backend → POST /v2/payment
        // If the API returns status: "CHALLENGE", your backend
        // redirects the browser to redirectAcsUrl.
        // After authentication, the 3DS provider redirects to
        // your successUrl or failUrl.
        fetch('/api/pay', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ cardToken: token, amount: 99.99 })
        });
      }
    }

    function handleError(response) {
      document.querySelectorAll('#cc-number, #cc-expiration, #cc-cvv')
        .forEach(el => el.classList.remove('is-invalid'));

      if (response.code === 'INVALID_PAN')
        document.querySelector('#cc-number').classList.add('is-invalid');
      else if (response.code === 'INVALID_EXPIRY_DATE')
        document.querySelector('#cc-expiration').classList.add('is-invalid');
      else if (response.code === 'INVALID_CVV')
        document.querySelector('#cc-cvv').classList.add('is-invalid');
    }
  </script>

</body>
</html>

Example B — PostMessage (SPA / embedded checkout)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Inyo Payment — PostMessage</title>
</head>
<body>

  <form id="checkout-form" novalidate>
    <div id="payment-form">
      <div>
        <label for="cc-name">Name on Card</label>
        <input type="text" data-field="cardholder" id="cc-name" required>
      </div>
      <div>
        <label for="cc-number">Card Number</label>
        <input type="text" data-field="pan" id="cc-number" maxlength="19" required>
      </div>
      <div>
        <label for="cc-expiration">Expiration</label>
        <input type="text" data-field="expirationDate" id="cc-expiration"
               placeholder="MM/YY" maxlength="5" required>
      </div>
      <div>
        <label for="cc-cvv">CVV</label>
        <input type="text" data-field="securitycode" id="cc-cvv"
               maxlength="4" required>
      </div>
      <button type="button" id="pay-btn">Pay $99.99</button>
    </div>
  </form>

  <!-- Hidden 3DS challenge iframe -->
  <div id="threeds-overlay" style="display:none; position:fixed; inset:0;
       background:rgba(0,0,0,0.5); z-index:1000; align-items:center;
       justify-content:center;">
    <iframe id="threeds-iframe" style="width:500px; height:600px;
            border:none; border-radius:8px; background:white;"
            sandbox="allow-forms allow-scripts allow-same-origin allow-popups">
    </iframe>
  </div>

  <script src="https://cdn.simpleps.com/sandbox/inyo.js"></script>
  <script>
    let tokenizer;

    document.addEventListener('DOMContentLoaded', () => {
      tokenizer = new InyoTokenizer({
        targetId: '#payment-form',
        publicKey: 'YOUR_PUBLIC_KEY',
        storeLaterUse: false,
        threeDSData: {
          enable: true,
          enablePostMessage: true   // ← No successUrl/failUrl needed
        },
        successCallback: handleTokenSuccess,
        errorCallback: handleTokenError
      });

      document.getElementById('pay-btn').addEventListener('click', () => {
        const form = document.getElementById('checkout-form');
        if (form.checkValidity()) {
          tokenizer.tokenizeCard();
        } else {
          form.classList.add('was-validated');
        }
      });

      // Listen for 3DS postMessage results
      window.addEventListener('message', handle3DSResult);
    });

    async function handleTokenSuccess(response) {
      if (response.reasonCode === 'WAITING_TRANSACTION') {
        const token = response.additionalData.token;

        // Call your backend to create the payment
        const res = await fetch('/api/pay', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ cardToken: token, amount: 99.99 })
        });
        const payment = await res.json();

        if (payment.status === 'CHALLENGE' && payment.redirectAcsUrl) {
          // Open 3DS challenge in the iframe overlay
          document.getElementById('threeds-overlay').style.display = 'flex';
          document.getElementById('threeds-iframe').src = payment.redirectAcsUrl;
        } else if (payment.status === 'AUTHORIZED' || payment.status === 'CAPTURED') {
          showSuccess(payment);
        } else {
          showError(payment.message || 'Payment declined.');
        }
      }
    }

    function handle3DSResult(event) {
      // Security: validate the message origin
      if (!event.origin.includes('simpleps.com')) return;

      const data = event.data;

      // Close the 3DS overlay
      document.getElementById('threeds-overlay').style.display = 'none';
      document.getElementById('threeds-iframe').src = '';

      if (data.approved === true || data.approved === 'true') {
        // Always verify on your backend before fulfilling
        verifyAndComplete(data.externalPaymentId);
      } else {
        showError('Card verification failed. Please try again.');
      }
    }

    async function verifyAndComplete(externalPaymentId) {
      const res = await fetch(`/api/payment/${externalPaymentId}/verify`);
      const payment = await res.json();
      if (payment.status === 'AUTHORIZED' || payment.status === 'CAPTURED') {
        showSuccess(payment);
      } else {
        showError('Payment could not be confirmed.');
      }
    }

    function handleTokenError(response) {
      document.querySelectorAll('#cc-number, #cc-expiration, #cc-cvv')
        .forEach(el => el.classList.remove('is-invalid'));

      if (response.code === 'INVALID_PAN')
        document.querySelector('#cc-number').classList.add('is-invalid');
      else if (response.code === 'INVALID_EXPIRY_DATE')
        document.querySelector('#cc-expiration').classList.add('is-invalid');
      else if (response.code === 'INVALID_CVV')
        document.querySelector('#cc-cvv').classList.add('is-invalid');
    }

    function showSuccess(payment) {
      alert(`Payment confirmed! ID: ${payment.paymentId || payment.externalPaymentId}`);
    }

    function showError(message) {
      alert(message);
    }
  </script>

</body>
</html>

Next Steps

With a token in hand, proceed to Authorizing a Card Payment to create the transaction via POST /v2/payment.