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:
- Load
inyo.jsin your payment page - Add
data-fieldattributes to your card input fields - Initialize
InyoTokenizerwith your public key and callbacks - Call
tokenizeCard()when the user submits — the library reads the form, encrypts the data, and returns a token - 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>
| 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.
<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.
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 providesuccessUrlorfailUrl. Instead, you listen formessageevents on the parent window after opening theredirectAcsUrlin an iframe. See Handling 3D Secure 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 setenablePostMessage.- PostMessage: Set
enablePostMessage: true. Do not setsuccessUrl/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:
| 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
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 thecardTokenId
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.
