---
description: >-
  Securely tokenize card data client-side using the Inyo JavaScript library.
  Tokens replace sensitive card details for PCI-compliant payment processing.
---

# 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](payment/pulling-funds/cards/authorizing/handling-3d-secure.md) for details.

> Contact your Inyo integration manager or [support@inyoglobal.com](mailto:support@inyoglobal.com) to register your URLs.

## Loading the Library

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

```html
<script
  src="https://cdn.simpleps.com/sandbox/inyo.js"
  integrity="sha384-..."
  crossorigin="anonymous">
</script>
```

| Environment | URL |
|---|---|
| **Sandbox** | `https://cdn.simpleps.com/sandbox/inyo.js` |
| **Production** | `https://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.

```html
<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-field` | Input | Notes |
|---|---|---|
| `cardholder` | Full name on card | As printed on the card |
| `pan` | Card number | 13–19 digits |
| `expirationDate` | Expiry | Format: `MM/YY` |
| `securitycode` | CVV/CVC | 3 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.

```javascript
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.

```javascript
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](payment/pulling-funds/cards/authorizing/handling-3d-secure.md) for the complete implementation guide.

### Configuration Parameters

| Parameter | Type | Required | Description |
|---|---|---|---|
| `targetId` | string | ✅ | CSS selector of the container holding the `data-field` inputs (e.g., `'#payment-form'`) |
| `publicKey` | string | ✅ | Your merchant public key, provided by Inyo |
| `successCallback` | function | ✅ | Called when tokenization succeeds |
| `errorCallback` | function | ✅ | Called when tokenization fails |
| `storeLaterUse` | boolean | ❌ | `false` (default) = one-time token; `true` = recurring/stored token |
| `threeDSData` | object | ❌ | 3D Secure configuration (see below) |

### 3DS Configuration (`threeDSData`)

| Field | Type | Required | Description |
|---|---|---|---|
| `enable` | boolean | ✅ | Whether to request 3DS authentication |
| `successUrl` | string | ⚠️ | **Redirect mode only.** URL where the 3DS provider redirects on success |
| `failUrl` | string | ⚠️ | **Redirect mode only.** URL where the 3DS provider redirects on failure |
| `enablePostMessage` | boolean | ⚠️ | **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](payment/pulling-funds/cards/authorizing/handling-3d-secure.md) for the full flow and code examples for both modes.

## Handling Callbacks

### Success Callback

```javascript
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:**

| Field | Description |
|---|---|
| `reasonCode` | `"WAITING_TRANSACTION"` when token is ready |
| `additionalData.token` | The card token UUID — use as `cardTokenId` in payment requests |
| `additionalData.lastFour` | Last 4 digits of the card number (for display) |

### Error Callback

```javascript
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:**

| Code | Description |
|---|---|
| `INVALID_PAN` | Card number is invalid (failed Luhn check or unsupported scheme) |
| `INVALID_EXPIRY_DATE` | Expiration date is invalid or card is expired |
| `INVALID_CVV` | Security code is invalid |

## One-Time vs. Recurring Tokens

| Token Type | `storeLaterUse` | Usage |
|---|---|---|
| **One-time** | `false` | Single transaction only — cannot be reused |
| **Recurring** | `true` | Can 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)

```html
<!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)

```html
<!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](payment/pulling-funds/cards/authorizing/) to create the transaction via `POST /v2/payment`.
