Table of Contents
Overview
First Data's Payment.js allows merchants working with various First Data APIs and gateways to tokenize payment credentials for later transactions without collecting, processing, or otherwise being able to view those payment credentials in their untokenized form, thus lowering their PCI compliance requirements.
Payment.js accomplishes this by injecting iframes into a parent form where customers can enter their data as though it were a normal form field styled however the merchant sees fit.
In form submission the client library, loaded into the parent window, sends one of the iframes a clientToken (for authentication with the service) and a RSA public key (asymmetric key pair). This iframe then collects the data hidden in the other iframes and encrypts the card number, expiration date, and cvv (the other fields are transferred without data layer encryption due to RSA message limits). This iframe then makes an API call to the Payment.js service for tokenization.
Assuming the customer is using a browser with modern cross-origin security controls, and these controls are not compromised by a browser defect, it will not be possible for non-Payment.js code to steal the data hidden in these iframes as the card number, expiration date, and cvv in particular never escape into the parent window in an unencrypted form.
When the tokenization request is sent out, only the already encrypted data will appear in the browser's network log.
- IPG
- Payeezy
- CardConnect (CardPointe/CardSecure)
- Bluepay
Note: The merchant's gateway credentials are never sent to the browser (encrypted or otherwise); The Payment.js client library utilizes a "clientToken" to associate the tokenization api call sent from the browser with credentials passed directly from merchant server to Payment.js server
Additional Security Settings Recommended
The following recommendations are to limit potential for fraudulent activity on your hosted payment page.
- Enable Re-Captcha
- Authentication to payment page
- Limit response back to the browsers/customer
- Enable appropriate Fraud Tools for your business type or payment flow
Functional Flow

- 01 Consumer visits merchant’s payment page; merchant will return payment page back to consumer’s web browser including the payment.js javascript library (referenced within merchant’s payment page script tag).
- 02 Merchant initiates an authorize session request; merchant sends in a few headers including a message signature that’s signed with their API secret and a nonce, timestamp etc. Also sends in their gateway credentials.
- 03 Payment.js server validates message signature + api key, generates an OAUTH token (called the clientToken). This has a one-time use with an expiration date. Payment.js server also requests a key-pair generated from a First Data crypto service; crypto service returns public key and key-pair id
- 04 Payment.js server creates a session record. Merchant gw credentials, key-pair id, and merchant-defined web hook stored for later use in the flow.
- 05 Public key + clientToken returned to merchant's server
- 06 Merchant returns payment form including iframed fields to client browser.
- 07 Consumer fills out and submits payment form. All payment fields (card number, cardholder name, expiration date, cvv2) encrypted with public key from pre flow
- 08 Payment.js server validates clientToken, then uses clientToken to do a lookup to grab the merchant’s gateway config. Session record containing merchant's session data deleted.
- 09 Payment.js server uses the key-pair ID from the sessionrecord together with the encrypted data from the consumer to request decryption from the First Data crypto service. Data decrypted and returned to Payment.js server
- 10 Payment.js server takes decrypted data, formats that into something that the target gateway accepts, and sends off a tokenization request to the gateway
- 11 Gateway processes tokenization request and returns response to Payment.js server.
- 12 Payment.js server forwards response to a merchant-defined web hook
- 13 Payment.js server also returns a HTTP 200 back to the client indicating that the tokenization request has completed. clientToken is now revoked and cannot be used for any further tokenization requests
- 14 Merchant webform should be updated to notify user that the tokenization request was successful or failed.
At this point, the payment.js flow has completed. Merchant would save token generated from the payment.js request in a database on their end and/or then do a subsequent auth or sale transactions using the token to charge the consumer.
Accessing the service
- uat: Customer Sandbox
- prod: Production aka Live
- 2.0.0
- uat: https://cert.api.firstdata.com/paymentjs/v2
- prod: https://prod.api.firstdata.com/paymentjs/v2
Referencing The Client Library
- https://lib.paymentjs.firstdata.com/{{env}}/client-{{version}}.js
- {{env}} = environment id
- {{version}} = version number
Registering with the service
In order to integrate with Payment.js, some details need to be registered on our end. The Payment.js apiKey and apiSecret would be provided by your First Data representative once boarded.
Integration Examples
Note: Payment.js does not impose platform restrictions (such as NodeJS) or require use of an SDK.
How To: Message Signature
- 01 HMAC SHA256 signed with Payment.js Api Secret
- 02 resulting HMAC hash needs to be hex encoded at this point
- 03 encode hex-encoded hash in Base64
- 01 Payment.js Api Key (corresponds to header "Api-Key")
- 02 Nonce (corresponds to header "Nonce")
- 03 Timestamp (corresponds to header "Timestamp")
- 04 payload (serialized; must match request body)
Note: Message components must be concatenated in order without delimiters.
Note: Payload must be serialized in the same way as it appears in the request body.
Code Examples
function genHmac(msg, secret) { const algorithm = CryptoJS.algo.SHA256); const hmac = CryptoJS.algo.HMAC.create(algorithm, secret); hmac.update(msg); const hexEncodedHash = hmac.finalize().toString(); const base64EncodedHash = base64.encode(hexEncodedHash); }
/** * @param string $msg Message To Sign * @param string $secret Payment.js Api Secret * @return string The value for header Message-Signature */ function genHmac($msg, $secret) { $algorithm = 'sha256'; $hexEncodedHash = hash_hmac($algorithm, $msg, $secret); $base64EncodedHash = base64_encode($hexEncodedHash); return $base64EncodedHash; }
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.digest.HmacAlgorithms; import org.apache.commons.codec.digest.HmacUtils; public class HmacGenerator { public String genHmac(String msg, String secret) { String algorithm = HmacAlgorithms.HMAC_SHA_256; HmacUtils hmacUtils = new HmacUtils(algorithm, secret); Hex hexEncoder = new Hex(); byte[] binaryEncodedHash = hmacUtils.hmac(msg); byte[] hexEncodedHash = hexEncoder.encode(binaryEncodedHash); String base64EncodedHash = Base64.encodeBase64String(hexEncodedHash); return base64EncodedHash; } }
API Reference: Authorize Session
Api Request
POST {{SERVICE_URL}}/merchant/authorize-session Api-Key: {{Payment.js-apikey}} Content-Type: application/json Message-Signature: {{MSG_SIGNATURE}} Nonce: {{random value}} Timestamp: {{timestamp, milliseconds since epoch}}
{ "gateway": "IPG", "apiKey": "", "apiSecret": "", "storeId": "", "zeroDollarAuth": false }
{ "gateway": "PAYEEZY", "apiKey": "", "apiSecret": "", "authToken": "", "transarmorToken": "", "currency": "USD", "zeroDollarAuth": false }
{ "gateway": "BLUEPAY", "accountId": "", "secretKey": "", "zeroDollarAuth": false }
{ "gateway": "CARD_CONNECT", "merchantId": "", "apiUserAndPass": "", "currency": "USD", "zeroDollarAuth": false }
Api Response
Client-Token: {{access/session id used by client library}} Nonce: {{should match "Nonce" header passed in request}}
{ "publicKeyBase64": "{{base64-encoded public rsa key used by client library}}" }
- HTTP 400: request failed structural validation (missing one or more expected fields/headers)
- HTTP 401: authentication error, may include payload with "error" field
Support for non-USD Currency
Pass an ISO 4217 currency code in field "currency" in the authorize session payload to perform a zero-dollar authorization under a currency other than "USD".
Note: Only applicable to Payeezy and CardConnect
Note: Please Click here for the list of ISO 4217 currencies supported by Payeezy
Note: Please Click here for the list of ISO 4217 currencies supported by CardConnect
Webhook
Description
The gateway response to the tokenization request is parsed for details, normalized, and then HTTPS POSTed to the configured webhook URL.
Note: A webhook url must support HTTPS and include fully qualified domain name as well as route
Note: A webhook url must only contain static elements
Webhook Payloads
Client-Token: {{access/session id for which webhook call applies}} Nonce: {{should match "Nonce" header passed in authorize session request}}
{ "authCode": "ABCABC", "card": { "bin": "424242", "brand": "visa", "exp": { "month": "02", "year": "2022" }, "last4": "4242", "name": "Blah Blah", "token": "123123123123", "masked": "XXXXXXXXXXXX4242", "address1": "Apartment 123", "address2": "123 Main Street", "city": "New York", "region": "NY", "country": "US", "postalCode": "11375", "company": "My Company" }, "gatewayRefId": "123123123123", "error": false, "zeroDollarAuth": { "cvv2": "MATCHED", "avs": "Y" } }
{ "error": true, "reason": "{{ERROR_CODE}}", "gatewayRefId": "...", "gatewayReason": "...", "zeroDollarAuth": { "cvv2": "NOT_MATCHED", "avs": "N" } }
- MATCHED
- NOT_MATCHED
- NOT_PROCESSED
- NOT_PRESENT
- NOT_CERTIFIED
- NO_RESPONSE
- "visa"
- "mastercard"
- "american-express"
- "diners-club"
- "discover"
- "elo"
- "jcb"
- "maestro"
- "mir"
- "unionpay"
- null: if ambiguous or unrecognized
- "BAD_REQUEST": the request body is missing or incorrect for endpoint
- "DECRYPTION_ERROR": failed to decrypt card data
- "INVALID_GATEWAY_CREDENTIALS": gateway credentials failed
- "JSON_ERROR": the request body is either not valid JSON or larger than 2kb
- "KEY_NOT_FOUND": no available key found
- "MISSING_CVV": zero dollar auth requires cvv in form data
- "NETWORK": gateway connection error
- "REJECTED": the request was rejected by the gateway
- "SESSION_CONSUMED": session completed in another request
- "SESSION_INSERT": failed to store session data
- "SESSION_INVALID": failed to match clientToken with valid record; can occur during deployment
- "UNEXPECTED_RESPONSE": the gateway did not respond with the expected data
- "UNKNOWN": unknown error
Client Library
Example Usage
<form id="form"> <!-- masked iframe value will be sent to webhook --> <div> <label for="first-data-payment-field-card">Card</label> <div id="cc-card" data-cc-card></div> </div> <!-- iframe value will be sent to webhook --> <div> <label for="first-data-payment-field-name">Name</label> <div id="cc-name" data-cc-name></div> </div> <!-- iframe value will be sent to webhook parsed into month and year--> <div> <label for="first-data-payment-field-exp">Exp</label> <div id="cc-exp" data-cc-exp></div> </div> <!-- required only if using cvv in config --> <!-- iframe value will not be sent to webhook --> <div> <label for="first-data-payment-field-cvv">CVV</label> <div id="cc-cvv" data-cc-cvv></div> </div> <!-- completely optional --> <!-- iframe value will be sent to webhook --> <div> <label for="first-data-payment-field-address1">Address1</label> <div id="cc-address1" data-cc-address1></div> </div> <!-- completely optional --> <!-- iframe value will be sent to webhook --> <div> <label for="first-data-payment-field-address2">Address2</label> <div id="cc-address2" data-cc-address2></div> </div> <!-- completely optional --> <!-- iframe value will be sent to webhook --> <div> <label for="first-data-payment-field-city">City</label> <div id="cc-city" data-cc-city></div> </div> <!-- completely optional --> <!-- iframe value will be sent to webhook --> <div> <label for="first-data-payment-field-region">State or Province</label> <div id="cc-region" data-cc-region></div> </div> <!-- completely optional --> <!-- iframe value will be sent to webhook --> <div> <label for="first-data-payment-field-country">Country</label> <div id="cc-country" data-cc-country></div> </div> <!-- completely optional --> <!-- iframe value will be sent to webhook --> <div> <label for="first-data-payment-field-postalCode">Postal Code</label> <div id="cc-postalCode" data-cc-postalCode></div> </div> <!-- completely optional --> <!-- iframe value will be sent to webhook --> <div> <label for="first-data-payment-field-company">Company</label> <div id="cc-company" data-cc-company></div> </div> <button id="submit">Submit</button> </form>
const config = { // optional styles: { ".emptyClass": { }, ".focusClass": { }, ".invalidClass": { color: "#C01324", }, ".validClass": { color: "#43B02A", }, }, // optional classes: { empty: "emptyClass", focus: "focusClass", invalid: "invalidClass", valid: "validClass", }, fields: { card: { selector: '[data-cc-card]', // optional placeholder: 'Credit Card Number', // optional, defaults to all brands being allowed. // see section titled "Restrict Card Brands" below for more information allowedBrands: ["visa", "discover", "mastercard", "american-express"], }, // optional but required for successful zero dollar auth cvv: { selector: '[data-cc-cvv]', // optional placeholder: 'CVV', }, exp: { selector: '[data-cc-exp]', // optional placeholder: 'Expiration Date', }, name: { selector: '[data-cc-name]', // optional placeholder: 'Full Name', }, // optional address1: { selector: '[data-cc-address1]', // optional placeholder: 'Address 1', } // optional address2: { selector: '[data-cc-address2]', // optional placeholder: 'Address 2', }, // optional city: { selector: '[data-cc-city]', // optional placeholder: 'City', }, // optional company: { selector: '[data-cc-company]', // optional placeholder: 'Company', }, // optional country: { selector: '[data-cc-country]', // optional placeholder: 'Country', }, // optional postalCode: { selector: '[data-cc-postalCode]', // optional placeholder: 'Postal Code', }, // optional region: { selector: '[data-cc-region]', // optional placeholder: 'State or Province', }, } };
const hooks = { // required preFlowHook: (callbackFn) => { // values come from authorize-session endpoint callbackFn({ clientToken: "....", publicKeyBase64: "...", }); }, // optional, alternate method of providing address field data // see target gateway documentation on how this data should be formatted // such as what country codes are accepted. submitFormHook: (callbackFn) => { callbackFn({ address1: "...", // optional address2: "...", // optional city: "...", // optional company: "...", // optional country: "...", // optional postalCode: "...",// optional region: "...", // optional }); }, };
window.firstdata.createPaymentForm(config, hooks, (paymentForm) => { // example: add submit handler to form formElement.addEventListener("submit", (e) => { e.preventDefault(); paymentForm.onSubmit( // on success (clientToken) => { }, // on failure (errorObj) => { }, ); }); });
Form Submission Flow
// onSubmit (roughly the manual equivalent) const onSubmit = (resolve, reject) => { try { paymentForm.validate( () => { paymentForm.authenticate( (auth) => { paymentForm.submit(auth, resolve, reject); }, reject, ); }, reject, ); } catch (error) { if (reject) { reject(error); } else { throw error; } } };
Styling Restrictions
The css properties that can be injected into the field iframes are restricted to the below whitelist.
- -moz-appearance
- -moz-osx-font-smoothing
- -moz-tap-highlight-color
- -moz-transition
- -webkit-appearance
- -webkit-font-smoothing
- -webkit-tap-highlight-color
- -webkit-transition
- -webkit-box-shadow
- -webkit-text-fill-color
- background-color
- appearance
- color
- direction
- font
- font-family
- font-size
- font-size-adjust
- font-stretch
- font-style
- font-variant
- font-variant-alternates
- font-variant-caps
- font-variant-east-asian
- font-variant-ligatures
- font-variant-numeric
- font-weight
- letter-spacing
- line-height
- opacity
- outline
- text-shadow
- transition
Resetting Form Fields
paymentForm.reset(() => console.log("reset form fields"));
Destroying Form Fields
paymentForm.destroyFields(() => console.log("destroyed form fields"));
Registering Card Brand Listener
// // data will contain the following details: // - brand: string // - brandNiceType: string // - code: string or object (may not be defined) // - field: string // - potentiallyValid: boolean // - selector: string // - valid: boolean // paymentForm.on("cardType", (data) => { console.log("card brand is: " + data.brandNiceType); });
Registering Field Focus Listener
// // data will contain the following details: // - field: string // - selector: string // paymentForm.on("focus", (data) => { console.log("field name is: " + data.field); });
Registering Field Blur Listener
// // data will contain the following details: // - field: string // - selector: string // paymentForm.on("blur", (data) => { console.log("field name is: " + data.field); });
Registering Field Change Listener
// // data will contain the following details: // - empty: boolean // - field: string // - length: number // - potentiallyValid: boolean // - selector: string // - touched: boolean // - valid: boolean // // if the field is the card number field, it will also have the following: // - brand: string // - brandNiceType: string // paymentForm.on("change", (data) => { console.log("field name is: " + data.field); });
Getting Form State
// // data will contain the following details for each field: // - empty: boolean // - field: string // - length: number // - potentiallyValid: boolean // - touched: boolean // - valid: boolean // // the card field object will also contain the following: // - brand: string // - brandNiceType: string // // the cvv field object will also contain the following: // - maxLength: number // paymentForm.getState((data) => { });
Set Field Focus
// // valid field names include: // - address1 // - address2 // - card // - city // - company // - country // - cvv // - exp // - name // - postalCode // - region // paymentForm.setFieldFocus(fieldName, () => console.log("set field focus for " + fieldName));
Restrict Card Brands
You may pass an array of brand identifiers as a string array in the form configuration to restrict the allowed card brands. If brands allowed are restricted in this way, the card field will fail the validation check if the entered number does not match one of the allowed brands. The configuration field is config.fields.card.allowedBrands as shown above under sub heading "Example Usage".
- visa
- mastercard
- american-express
- diners-club
- discover
- elo
- jcb
- maestro
- mir
- unionpay
Known Issues
Set field focus
The setFieldFocus client library functionality does not currently work in Safari.
Clicking label to focus field
While giving label elements particular "for" properties (as seen in the example html) is supposed to enable focusing the field by clicking the label, this does not work in Safari.
NVDA can prevent field entry
When tabbing through all the iframe fields and then tabbing back through them again NVDA can get stuck in "browse" mode which by default NVDA configuration means field entry is blocked by NVDA since it intercepts all the key presses. To resolve the issue the NVDA user can press a key or key combination that triggers "focus" mode such as "space" or "enter". See NVDA documentation on other key combinations that can be used or configured.
Autofill limitations
Previously we supported autofilling multiple payment fields simultaneously by using browser autofill on the card number field. We had to remove that functionality as it caused an issue on mobile devices where some hidden fields became focusable. Autofill now only works on a field by field basis with the iframe fields.
Validations not firing on focus change in iOS 12 and below
In iOS 12 and below the "blur" event does not fire when focus changes from one iframe to another iframe or window. We target the "blur" event to check for incomplete field input such as when a card number of insufficient length is entered. Validations will still occur as expected when form submission occurs. This bug was fixed in iOS 13.
Formatting differences on card field on Android 10 & 11
Normally the card field automatically groups the input digits with a space separating the groupings. This is intended to improve the readability of the field as end-users input their credit card information. We had to disable this on Android 10 & 11 due to an issue found in Samsung Android devices where input keys would sometimes be double-entered and another issue where backspacing led to the cursor jumping over one of the digits in some circumstances. All other supported user agents should be unaffected by this change.
Release Notes
Click here for release notes.