Browsing the USA ePay documentation, I found out that there were two ways to geneate a credit card token.
The first is one that uses a jQuery ueForm JSONP endpoint that isn't very well documented and from the looks of it is no longer supported, however the endpoints still work.
This seems to be USA ePays first attempt at the client-side tokenizer.
I threw together a simple test platform, as shown below:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Single Use Token JSONP Test</title> <meta name="viewport" content="width=device-width, initial-scale=1"> </head> <body> <label for="creditcardnumber">Credit Card Number</label> <input type="text" name="creditcardnumber" id="creditcardnumber" value="4444333322221111" /><br/> <label for="cvc">CVC</label> <input type="text" name="cvc" id="cvc" value="123" /><br/> <label for="expirationdate">Expiration Date</label> <input type="text" name="expirationdate" id="expirationdate" value="09/25" /><br/> <label for="postalcode">Postal Code</label> <input type="text" name="postalcode" id="postalcode" value="123" /><br/> <label for="address">CVC</label> <input type="text" name="address" id="address" value="123 Test St" /><br/> <label for="singleusetoken">Single Use Token</label> <input type="text" name="singleusetoken" id="singleusetoken" disabled placeholder="Will Be Generated..." /><br/> <button id="submitButton">Submit Payment</button> <script> var callbackName = 'callback'; var timeoutMs = 5000; var timeoutTrigger = window.setTimeout(function () { window[callbackName] = function () {}; console.error('JSONP timeout'); }, timeoutMs); window[callbackName] = function (_data) { window.clearTimeout(timeoutTrigger) var data = _data || {}; if (typeof data.token !== 'undefined' && data.token !== '') { var singleUseTokenInput = document.getElementById('singleusetoken'); singleUseTokenInput.value = data.token } else { console.error(data); } } var buttonElm = document.getElementById('submitButton'); buttonElm.addEventListener('click', function() { var result = []; var addressInput = document.getElementById('address') var creditCardNumerInput = document.getElementById('creditcardnumber') var cvcInput = document.getElementById('cvc') var expirationDateInput = document.getElementById('expirationdate') var postalCodeInput = document.getElementById('postalcode') var jsonpBody = { address: addressInput.value, ccnum: creditCardNumerInput.value, cvv2: cvcInput.value, expdate: expirationDateInput.value, zip: postalCodeInput.value } Object.keys(jsonpBody).forEach(function (key) { var param = jsonpBody[key]; if (typeof param !== 'undefined') { result.push(encodeURIComponent(key) + '=' + encodeURIComponent(param)); } }); var reqUrl = 'https://sandbox.usaepay.com/tokenize?' + result.join('&'); var script = document.createElement('script'); script.type = 'text/javascript'; script.async = true; script.src = reqUrl; script.onerror = function (error) { console.error(error) }; document.getElementsByTagName('head')[0].appendChild(script); }); </script> </body> </html>
Here is the actual jQuery code used to create it:
https://sandbox.usaepay.com/tokenize/jquery.ueform.js
I haven't created a test platform for this, but the code should at least give an idea for what it does.
Here is some doc I was able to find relating to this endpoint:
The new one I found and the way I will be creating tokens for this project is the Payment Key Sale endpoint. It also has a fairly substantial amount of documentation.
This implementation uses an iFrame and harnesses a good amount of window.postMessage
to communicate things like the resulting credit card token, validation errors, and response codes.
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Payment Key Sale Test</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> #creditCardWrapper { display: none; } </style> </head> <body> <label for="amount">Amount</label> <input type="text" name="amount" /><br/> <div id="creditCardWrapper"> <label for="paymentCardContainer">Credit Card Information</label> <div id="paymentCardContainer"></div> </div> <label for="paymentkey">Payment Key</label> <input type="text" name="paymentkey" id="paymentkey" disabled placeholder="Will Be Generated..." /><br/> <label for="cardbrand">Card Brand</label> <input type="text" name="cardbrand" id="cardbrand" disabled placeholder="Will Be Generated..." /><br/> <button id="submitPayment"> Submit Payment </button> <script> document.getElementById('submitPayment').addEventListener('click', function (e) { e.preventDefault(); document.getElementById("paymentCardIFrame").contentWindow.postMessage("generateToken::true", "*") }) var paymentFrameStyles = function (showFieldError = false) { return ` html, body { margin: 0; padding: 0; } #payjs-container { display: flex; } #payjs-container .payjs-input-icon { display: none; } #payjs-container #payjs-cnum { border: 1px ${showFieldError === true ? 'red': 'black'} solid; border-radius: 3px; padding: 15px; width: 60%; } #payjs-container #payjs-exp { margin-left: 15px; border: 1px ${showFieldError === true ? 'red': 'black'} solid; border-radius: 3px; padding: 15px; width: 20%; } #payjs-container #payjs-cvv { margin-left: 15px; border: 1px ${showFieldError === true ? 'red': 'black'} solid; border-radius: 3px; padding: 15px; width: 20%; } ` } var usaEpayPaymentKeyIframeContentWindow = null; var usaEpayPaymentKeyIframe = document.createElement('iframe'); usaEpayPaymentKeyIframe.id = 'paymentCardIFrame'; usaEpayPaymentKeyIframe.frameBorder = 0; usaEpayPaymentKeyIframe.style.height = '32px'; usaEpayPaymentKeyIframe.style.width = '100%'; usaEpayPaymentKeyIframe.onload = function() { usaEpayPaymentKeyIframeContentWindow = document.getElementById("paymentCardIFrame").contentWindow; var paymentKeyFrameSettings = { extended_response: true } usaEpayPaymentKeyIframeContentWindow.postMessage("addSettings::" + JSON.stringify(paymentKeyFrameSettings), "*") usaEpayPaymentKeyIframeContentWindow.postMessage("addStyles::" + paymentFrameStyles(), "*") document.getElementById('creditCardWrapper').style.display = 'block'; } usaEpayPaymentKeyIframe.src = 'https://sandbox.usaepay.com/js/v1/card.html#_JJ5n1IzpVzkz0qmQ60xO6mQ2DDgk29KcI5X66ILB5'; document.getElementById('paymentCardContainer').appendChild(usaEpayPaymentKeyIframe); window.addEventListener("message", function(event) { if (("string" == typeof event.data || event.data instanceof String) && -1 !== event.data.indexOf("::") ) { const tokens = event.data.split('::') switch (tokens[0]) { case 'error': usaEpayPaymentKeyIframeContentWindow.postMessage("addStyles::" + paymentFrameStyles(true), "*") break; case 'ready': // noop break; case 'paymentKey': var cardBrandInput = document.getElementById('cardbrand'); var paymentKeyInput = document.getElementById('paymentkey'); const extendedResponse = JSON.parse(tokens[1]) paymentKeyInput.value = extendedResponse.key; cardBrandInput.value = extendedResponse.creditcard.cardtype; usaEpayPaymentKeyIframeContentWindow.postMessage("addStyles::" + paymentFrameStyles(), "*") default: break } } }) </script> </body> </html>
The Payment Key Sale implementation is obviously much more PCI secure than JSONP.
It also conforms to PCI DSS while JSONP does not; credit card data is still technically being handled by code managed by my business. If we were to use the JSONP solution, there would need to be things like web application firewalls, separate VPCs, code scanning, the list goes on.
Dealing with PCI vendors can be tricky, especially if you talk to sales people. Try to get the technical experts or engineers as they will be more informed as to what's actually required and what you can get away with instead of being sold on a bunch of stuff you don't need. I may come back to this article in the future and elaborate more, but for now I just wanted to bring this up.