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.

The following gateways are supported:
  • 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

Diagram 1: Data 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
API Service URLs
  • uat: https://cert.api.firstdata.com/paymentjs/v2
  • prod: https://prod.api.firstdata.com/paymentjs/v2

Referencing The Client Library

The "src" attribute for the script tag follows the following format:
  • 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
Message Components
  • 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

Message Signature: Javascript
function genHmac(msg, secret) {
  const algorithm = CryptoJS.algo.SHA256);
  const hmac = CryptoJS.algo.HMAC.create(algorithm, secret);
  const hexEncodedHash = hmac.finalize().toString();
  const base64EncodedHash = base64.encode(hexEncodedHash);
Message Signature: PHP
 * @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;
Message Signature: Java
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

Method, URL, and Headers
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}}
Payload for IPG
  "gateway": "IPG",
  "apiKey": "",
  "apiSecret": "",
  "storeId": "",
  "zeroDollarAuth": false
Payload for Payeezy
  "gateway": "PAYEEZY",
  "apiKey": "",
  "apiSecret": "",
  "authToken": "",
  "transarmorToken": "",
  "currency": "USD",
  "zeroDollarAuth": false
Payload for Bluepay
  "gateway": "BLUEPAY",
  "accountId": "",
  "secretKey": "",
  "zeroDollarAuth": false
Payload for CardConnect
  "gateway": "CARD_CONNECT",
  "merchantId": "",
  "apiUserAndPass": "",
  "currency": "USD",
  "zeroDollarAuth": false

Api Response

Noteworthy Response Headers
Client-Token: {{access/session id used by client library}}
Nonce: {{should match "Nonce" header passed in request}}
Response Success Payload
  "publicKeyBase64": "{{base64-encoded public rsa key used by client library}}"
Error Cases
  • 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



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

Noteworthy Headers
Client-Token: {{access/session id for which webhook call applies}}
Nonce: {{should match "Nonce" header passed in authorize session request}}
Example Success Payload
  "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 payload
  "error": true,
  "reason": "{{ERROR_CODE}}",
  "gatewayRefId": "...",
  "gatewayReason": "...",
  "zeroDollarAuth": {
    "cvv2": "NOT_MATCHED",
    "avs": "N"
CVV2 Auth Codes
Possible card brand values
  • "visa"
  • "mastercard"
  • "american-express"
  • "diners-club"
  • "discover"
  • "elo"
  • "jcb"
  • "maestro"
  • "mir"
  • "unionpay"
  • null: if ambiguous or unrecognized
Error Codes
  • "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 with placeholders
<form id="form">
  <!-- masked iframe value will be sent to webhook -->
    <label for="first-data-payment-field-card">Card</label>
    <div id="cc-card" data-cc-card></div>

  <!-- iframe value will be sent to webhook -->
    <label for="first-data-payment-field-name">Name</label>
    <div id="cc-name" data-cc-name></div>

  <!-- iframe value will be sent to webhook parsed into month and year-->
    <label for="first-data-payment-field-exp">Exp</label>
    <div id="cc-exp" data-cc-exp></div>

  <!-- required only if using cvv in config -->
  <!-- iframe value will not be sent to webhook -->
    <label for="first-data-payment-field-cvv">CVV</label>
    <div id="cc-cvv" data-cc-cvv></div>

  <!-- completely optional -->
  <!-- iframe value will be sent to webhook -->
    <label for="first-data-payment-field-address1">Address1</label>
    <div id="cc-address1" data-cc-address1></div>

  <!-- completely optional -->
  <!-- iframe value will be sent to webhook -->
    <label for="first-data-payment-field-address2">Address2</label>
    <div id="cc-address2" data-cc-address2></div>

  <!-- completely optional -->
  <!-- iframe value will be sent to webhook -->
    <label for="first-data-payment-field-city">City</label>
    <div id="cc-city" data-cc-city></div>

  <!-- completely optional -->
  <!-- iframe value will be sent to webhook -->
    <label for="first-data-payment-field-region">State or Province</label>
    <div id="cc-region" data-cc-region></div>

  <!-- completely optional -->
  <!-- iframe value will be sent to webhook -->
    <label for="first-data-payment-field-country">Country</label>
    <div id="cc-country" data-cc-country></div>

  <!-- completely optional -->
  <!-- iframe value will be sent to webhook -->
    <label for="first-data-payment-field-postalCode">Postal Code</label>
    <div id="cc-postalCode" data-cc-postalCode></div>

  <!-- completely optional -->
  <!-- iframe value will be sent to webhook -->
    <label for="first-data-payment-field-company">Company</label>
    <div id="cc-company" data-cc-company></div>

  <button id="submit">Submit</button>
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
      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) => {
      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) => {
      // on success
      (clientToken) => {

      // on failure
      (errorObj) => {

Form Submission Flow

// onSubmit (roughly the manual equivalent)
const onSubmit = (resolve, reject) => {
  try {
      () => {
          (auth) => {
            paymentForm.submit(auth, resolve, reject);


  } catch (error) {
    if (reject) {
    } else {
      throw error;

Styling Restrictions

The css properties that can be injected into the field iframes are restricted to the below whitelist.

Style 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".

Brand Identifiers
  • 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.