Tokenizing a card in the browser

After your server has created an invoice for the customer's purchase, it must send back a payment page to let the customer choose how they want to pay. The page will generally contain a form for the customer to enter card details or select a card they have previously stored.

When the customer has submitted their card details, the API returns a card token string to your code. The token
uniquely identifies the set of details but it is essentially a "random" value that does not reveal any private information. Using a token to hide card details in this way is known as tokenizing the card. For security, Smart Router does not let you access the card details themselves, so you must use the token to identify the card and capture payments from it.

The code fragment below is a complete example that demonstrates several aspects of tokenizing a card, such as:

The sections that follow the example explain how this code is constructed, one aspect at a time.

<div class="container">
  <form action="" method="POST" id="card-form">
    <div class="block-group">
			<input type="text" placeholder="John Smith" class="b75 block" id="cardholdername">
      <input type="text" placeholder="10018" class="b25 block" id="cardholderzip">
		</div>

    <div class="block-group">
      <div class="block" data-processout-input="cc-number" 
      data-processout-placeholder="4242 4242 4242 4242"></div>
      <div class="block" data-processout-input="cc-exp" 
        data-processout-placeholder="MM / YY"></div>
      <div class="block" data-processout-input="cc-cvc" 
        data-processout-placeholder="CVC"></div>
    </div>

    <div>
      <input type="submit" id="paymentBtn">
    </div>
    <div id="errors"></div>
    <div id="success"></div>
  </form>
</div>
/*! PocketGrid 1.1.0
* Copyright 2013 Arnaud Leray
* MIT License
*/
* {
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

/* Clearfix */
.block-group {
  *zoom: 1;
}
.block-group:before, .block-group:after {
  display: table;
  content: "";
  line-height: 0;
}
.block-group:after {
  clear: both;
}

.block-group {
  /* ul/li compatibility */
  list-style-type: none;
  padding: 0;
  margin: 0;
}

/* Nested grid */
.block-group > .block-group {
  clear: none;
  float: left;
  margin: 0 !important;
}

/* Default block */
.block {
  float: left;
  width: 100%;
}
.b75 {
  width: 74%;
}
.b25 {
  width: 25%;
  margin-left: 1%;
}

html, body {
  background: #ECEFF1;
  font-size: 16px;
}

.container {
  width: 100%;
  max-width: 400px;
  background: white;
  margin: 3em auto;
  padding: 1em;
  border-radius: 4px;
  box-shadow: 0 5px 7px rgba(50, 50, 93, 0.04), 0 1px 3px rgba(0, 0, 0, 0.03);
}

[data-processout-input], input {
  border: 1px solid #ECEFF1;
  border-radius: 4px;
  box-shadow: 0 5px 7px rgba(50, 50, 93, 0.04), 0 1px 3px rgba(0, 0, 0, 0.03);
  padding: 0.5em;
  width: 100%;
  margin-bottom: 1em;
  font-size: 14px;
  min-height: 2em;
}
::-webkit-input-placeholder { /* WebKit, Blink, Edge */
  color:    #ECEFF1;
}
:-moz-placeholder { /* Mozilla Firefox 4 to 18 */
  color:    #ECEFF1;
  opacity:  1;
}
::-moz-placeholder { /* Mozilla Firefox 19+ */
  color:    #ECEFF1;
  opacity:  1;
}
:-ms-input-placeholder { /* Internet Explorer 10-11 */
  color:    #ECEFF1;
}
[data-processout-input]:nth-child(1) {
  width: 48%;
  display: inline-block;
}
[data-processout-input]:nth-child(2) {
  width: 25%;
  display: inline-block;
  margin-left: 1%;
}
[data-processout-input]:nth-child(3) {
  width: 25%;
  display: inline-block;
  margin-left: 1%;
}

input[type="submit"] {
  margin: 0;
  box-shadow: 0 5px 7px rgba(50, 50, 93, 0.04), 0 1px 3px rgba(0, 0, 0, 0.03);
  background: #3F51B5;
  color: white;
  border-radius: 4px;
  border: 1px solid #303F9F;
}

#errors, #success {
  margin-top: 1em;
  text-align: center;
  font-size: 0.9em;
  color: #D84315;
}
#success {
  color: #4CAF50;
}
document.addEventListener("DOMContentLoaded", function() {
    var client = new ProcessOut.ProcessOut("test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x");
    var formElement = document.getElementById("card-form");
    
    style = {
      fontSize: "14px",
      "::placeholder": {
        color: "#ECEFF1",
      },
    }

    client.setupForm(formElement, {
      style: style
    }, processoutReadyHandler, function(err) {
      console.log(err);
    });

    
    function processoutReadyHandler(form) {
      form.getNumberField().on("focus", function(e) {
        document.getElementById("errors").innerHTML = "";
      });
      form.getExpiryField().on("focus", function(e) {
        document.getElementById("errors").innerHTML = "";
      });
      form.getCVCField().on("focus", function(e) {
        document.getElementById("errors").innerHTML = "";
      });

      form.addEventListener("submit", function(e) {
        e.preventDefault();
        document.getElementById("paymentBtn").disabled = true;

        // Let's tokenize the card
        client.tokenize(form, {
          name: document.getElementById("cardholdername").value,
          contact: {
            zip: document.getElementById("cardholderzip").value
          }
        }, function(token) {
          document.getElementById("success").innerHTML = "Success! Your created card token is "+token;
          document.getElementById("errors").innerHTML = "";
          document.getElementById("paymentBtn").disabled = false;
        }, function(err) {
          document.getElementById("errors").innerHTML = err.message;
          document.getElementById("success").innerHTML = "";
          document.getElementById("paymentBtn").disabled = false;
        });

        return false;
      });
    }
});

Tokenization flow

Below is a sequence diagram that shows the flow of the tokenization process. The following sections explain the flow in more detail.

Setting up the form

The Smart Router API is designed to set up the card detail fields on the payment form automatically and receive data from them securely. Your own JavaScript code has no access to the customer's private data because the API directly gathers and encrypts this data to store in the ProcessOut vault. However, your code still has the flexibility to add styling to the secure fields and respond to events from them.

The following HTML sample shows the layout of a typical payment form. Note that the secure card fields are <div> elements rather than <input> fields. The API adds its own <iframe> elements
inside the <div> elements to receive the card details in accordance with our security standards .
Add a data-processout-input attribute to each <div> to specify which card field to generate. The possible values of this attribute are:

  • cc-number: the main card number (16 digits)
  • cc-exp-month: the month of the expiration date
  • cc-exp-year: the year of the expiration date
  • cc-exp: the full expiration date in mm/yy format
  • cc-cvc: the CVC number

Use standard HTML <input> fields for other non-secure form elements such as the Submit button.

<form action="/your-endpoint" method="POST" id="card-form">
  <div data-processout-input="cc-number"
        data-processout-placeholder="4242 4242 4242 4242"></div>
  <div data-processout-input="cc-exp-month"
        data-processout-placeholder="Expiration month"></div>
  <div data-processout-input="cc-exp-year"
        data-processout-placeholder="Expiration year"></div>
  
  <!-- You can also use a single combined field for the expiration date: -->
  <div data-processout-input="cc-exp"
        data-processout-placeholder="Expiration date (mm / yy)"></div>


  <div data-processout-input="cc-cvc"
        data-processout-placeholder="CVC"></div>

  <input type="submit" class="submit" value="Submit Payment">
</form>

In your page's JavaScript, use the ProcessOut.setupForm() function to set up the form with the secure fields. Firstly, load the processout.js library into your page. Then, add the following code in a separate <script> element. Placing the code in the DOMContentLoaded event listener ensures that it will run as soon as the page has finished loading. Note that the processoutReadyHandler function is a callback that runs when the form is correctly set up. It is described in the next section.

document.addEventListener("DOMContentLoaded", function() {
  var client = new ProcessOut.ProcessOut(
    "test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x");

  var formElement = document.getElementById("card-form");

  client.setupForm(
    formElement,          // The <form> element in your page.
    {
      // Form options. You can also add other options such as
      // styling (see "Styling the form" below).
      requireCVC: true,
      // Form options. By default CVC field will be auto
      // focused when expiry field is filled. You can turn 
      // off this default behavior. Note that this auto focus functionality
      // is not supported in mobile and desktop Safari.
      expiryAutoNext: false, 
      // By default expiry field will be focused when card number if filled.
      // You can turn off this behavior. Note that this auto focus functionality
      // is not supported in mobile and desktop Safari.
      cardNumberAutoNext: false,
    },
    processoutReadyHandler, // Callback for successful setup.
    
    // Error callback.
    function(err) {
      console.log("Whoops, couldn't setup the form: "+err);
    }
  );
});

The options available for ProcessOut.makeCardPayment() are shown in the table below.

OptionDescription
require_cvcIf true then the user must enter a CVC before submitting the form.
styleObject containing CSS styles to apply to the form (see Styling the form below).

Generating the card token

ProcessOut.setupForm() calls a callback function (named processoutReadyHandler in the example above) after it has set up the card fields. The CardForm object that you use to tokenize the card is passed to processoutReadyHandler as a parameter.

In the example below, processoutReadyHandler adds an event listener that responds when the form is submitted. This listener calls CardForm.tokenize() to generate the token. CardForm.tokenize() itself also has a callback that receives the token string as a parameter and adds it into the form as a hidden field.

function processoutReadyHandler(form) {
  // The form is fully loaded at this point.
  formElement.addEventListener("submit", function(e) {
    // Cancel the default behavior of the Submit button.
    e.preventDefault();
    // Blocking the form while performing the tokenization may also
    // avoid race conditions (eg, when
    // the user double-clicks the button).
    document.getElementById("paymentBtn").disabled = true;

    // Tokenize the card
    client.tokenize(form, {
      // You can also optionally send other information about
      // the cardholder.
      name: document.getElementById("cardholdername").value,
      contact: {
        zip: document.getElementById("cardholderzip").value,
        // Available contact fields:
        // address1, address2, city, state, country_code, zip
      },

      // You can offer the customer a preferred scheme
      // to pay on, say, if the customer's card supports 
      // co-schemes such as carte bancaire.
      preferred_scheme: "carte bancaire"
    }, function(token, card) {
      var field   = document.createElement("input");
      field.type  = "hidden";
      field.name  = "token";
      field.value = token;

      // Re-enable the button
      document.getElementById("paymentBtn").disabled = false;

      // Add the token input so that it gets sent back to the server.
      formElement.appendChild(field);

      // Finally, submit the form.
      formElement.submit();
    }, function(err) {
      // As well as the message, the error object contains
      // a code value to denote the specific type of error
      // (see the next section for details).
      alert(err.message);

      // Re-enable the button
      document.getElementById("paymentBtn").disabled = false;
    });

    return false;
  });
}

Handling errors

The examples so far handle errors by simply printing out the error message stored in the err.message attribute. The error object also has a code attribute that identifies the type of error. The most common values for err.code are:

  • card.declined: The card was declined by the PSP
  • card.expired: The card is past its expiration date
  • card.invalid: The card is invalid
  • card.invalid-number: The supplied card number is invalid
  • card.invalid-date: The supplied expiration date is invalid
  • card.invalid-month: The month of the supplied expiration date is invalid
  • card.invalid-year: The year of the supplied expiration date is invalid
  • card.invalid-cvc: The supplied CVC number is invalid

If you use a switch statement to handle these errors (as shown below) then you should include a default case because other values for err.code can sometimes occur.

client.tokenize(form, { ... }, function(token) { ... }, function(err) {
  switch (err.code) {
    case "card.declined":
      // Code to handle a declined card
      break;
    case "card.expired":
      // Code to handle an expired card
      break;

      ...

    default:
    // Code to handle uncommon errors
  }
});

Styling the form

You can add a style attribute to the options parameter of ProcessOut.setupForm(). The style object lets you supply a limited subset of CSS styles for the secure form fields.

var style = {
  fontSize: "14px",
  "::placeholder": {
    color: "#ECEFF1"
  }
};

client.setupForm(formElement, {
    style: style
  }, processoutReadyHandler, function(err) { ... }
);

By default, the secure fields are generated with very little styling. The background for the fields is transparent and there is no margin or padding. This lets you manage these CSS properties using the parent elements in your form (ie, the <div> elements from the example given above).

You can apply the following CSS properties to the secure form fields using the style option:

  • color
  • direction
    • Setting this to rtl will apply direction: rtl; to the CVC secure field, and will apply text-align: right; to all the secure fields. This is to ensure a consistent formatting of the card number and expiry fields when utilising this RTL support.
  • fontFamily
  • fontSize
  • fontSizeAdjust
  • fontStretch
  • fontSmoothing
  • fontStyle
  • fontVariant
  • fontWeight
  • fontHeight
  • lineHeight
  • textShadow
  • textTransform
  • textDecoration
  • transition

You can also use the :hover and :focus pseudo-classes and the ::placeholder and ::selection pseudo-elements by adding them as sub-objects of the style object, as shown in the example.

ProcessOut.js also supports custom fonts for enterprise users. Contact us if you want to use your own fonts and branding for the secure form fields.

Handling form events

The form object passed to processoutReadyHander lets you access the secure form fields using the following
functions:

  • CardForm.getNumberField()
  • CardForm.getCVCField()
  • CardForm.getExpiryField()
  • CardForm.getExpiryMonthField()
  • CardForm.getExpiryYearField()

For security reasons, the objects returned by these functions give you only limited access to the fields. However, you can add event listeners to them using the on() function, as shown below. The listeners let you respond to events for the secure fields in the usual way.

client.setupForm(formElement, {},
  processoutReadyHander,
  function(err) { ... }
);

function processoutReadyHander(form) {
  form.getNumberField().on("click", function(e) {
    console.log(e);
  });
  form.getExpiryField().on("click", function(e) {
    console.log(e);
  });
  form.getCVCField().on("click", function(e) {
    console.log(e);
  });
}

Each event will include useful information about the current state of the card form.

Events can be used to dynamically build a card scheme display or dynamically validating the card:

function processoutReadyHander(form) {
  form.getNumberField().on("click", function(e) {
    console.log(e);
    
      /*
        Will print the following structure:

        {
          card_number_length: 16
          max_card_number_length: 19,
          min_card_number_length: 12,
          potentially_valid: true,
          valid: false,
          schemes: ["visa", "carte bancaire"]
        }
      */

    
  });
}

Event structure detail

FieldDescriptionTypeExample value
card_number_lengthCurrent length of the card fieldInteger16
max_card_number_lengthMaximum length of the card according to its BINInteger19
min_card_number_lengthMinimum length of the card according to its BINInteger12
potentially_validtrue if the current value can match a valid card number once completedBooleantrue
validtrue when the card passes the Luhn checkBooleantrue
schemesList of possible schemes according to the card BINList["visa", "carte bancaire"]

Next steps

Once you have generated the card token successfully, the next step is to use it to capture a payment
from the customer. You may also be interested in saving a token to capture future payments.