iFrame Payment Form v0.5

There are a variety of ways to send payment data to Spreedly. However, to limit your PCI compliance scope while still retaining full control of the UI, the recommended approach is to use an embedded iFrame form. This form, served by Spreedly, collects the sensitive portions of your customers’ payment data and stores it in your Spreedly vault without it ever touching your server environment.

Since browsers implement strict security guarantees that prevent cross domain pages from leaking data, the interaction between the host page (your order or payment page) and the Spreedly iFrame (the credit card form) must pass through a specific and pre-determined channel. This channel is managed by the Spreedly payment-frame.js library and exposes a set of hooks that allow you to style the iFrame form to fit your UI and extract tokenized payment data.

Before you begin

If, at any point, you experience difficulty implementing or customizing your payment form, please refer to the sample app to see a working example.

When building a checkout form that uses Spreedly’s iFrame payment frame, think about the integration as the marriage of a master form (the form you serve from your server environment that collects non-sensitive data like your customer’s name and address) and a payment form (the form Spreedly serves that only has fields for the card number and CVV). Although they are two distinct forms served by different systems, they are meant to work in concert to present a unified experience to your users.

Here are a few best practices we recommend:

  • Complex checkout forms are problematic on a variety of fronts: They are difficult to implement, have a high abandonment rate, and make error handling difficult. Make the payment portion of your checkout process as simple as possible.
  • Avoid dynamic features such as inline validation or animations. These are not supported by the payment form inside the iFrame and the inconsistency may confuse users.

With these in mind, the first step is to integrate the payment frame into your master payment form.

Master form

The iFrame payment form only contains fields for the user’s card number and CVV (these are the only PCI-scoped fields). Any other information you wish to collect as part of the payment process must be defined in your master form. For this example we’re assuming the collection of the following fields which, in addition to the card number, are required by Spreedly for a valid payment method:

  • First name
  • Last name
  • Card expiration month and year

To start with, create an HTML page representing your payment form (this page, served by your systems on your domain, will be referred to as the “master form”).


<!-- Include the payment-frame library on your page -->
<script src="https://core.spreedly.com/payment-frame/payment-frame-0.5.min.js"></script>

<!-- Your master form should have a custom onsubmit handler -->
<form id="payment-form" accept-charset="UTF-8" action="https://yoursite.com/order/new" method="POST"
  onsubmit="submitPaymentForm(); return false;">

  <!-- This field will get the payment method token value after the user submits the payment frame -->
  <input type="hidden" name="payment_method_token" id="payment_method_token" value="" />

  <label for="first_name">First name</label>
  <input type="text" id="first_name" name="first_name" /><br/>

  <label for="last_name">Last name</label>
  <input type="text" id="last_name" name="last_name" />

  <!-- This is where the CC number and CVV fields will appear -->
  <div id='frame-holder' style="height: 40px;">
    <iframe id="spreedly-iframe" frameborder="0" width="100%" height="100%" scrolling="no"></iframe>
  </div>

  <!-- You are responsible for collecting required, but not PCI-sensitive data, such as the card exp mm/yyyy -->
  <label for="month">Expiration Date</label>
  <input name="month" type="text" id="month" size="3"> /
  <input name="year" type="text" id="year" size="4">
  <br/>

  <input type="submit" />
</form>

You’ll notice the following:

  • You need to include the payment-frame.js library on your page with <script src="https://core.spreedly.com/payment-frame/payment-frame-0.5.min.js"></script>
  • The master form has a custom onsubmit handler that will manage the submission process (its implementation is covered later).
  • The master form will have input elements for all the non-sensitive data you wish to collect as part of the checkout process (here the first and last name, and card expiration month and year).
  • The master form should also have a hidden input (<input type="hidden" name="payment_method_token" id="payment_method_token" />) that will contain the token representing the user’s payment method in the Spreedly vault once it’s been tokenized.
  • Although the payment frame form will be submitted directly to Spreedly, your master form should have an action that submits it directly to your server environment. Since the user’s payment will have first been tokenized by Spreedly, it is safe to do so.
  • Embedded within the master form is the Spreedly payment iFrame, which is where the CC number and CVV fields will appear. You should leave the src attribute empty (or set it to about:blank). The proper frame source URL will be automatically set by the payment-frame.js library.
  • There is some extra HTML (e.g., the frame-holder div) and default styling present in this example as well. None of it is mandatory, but read further before ripping it out.

With the master form HTML in place you need to initialize the payment-frame library and bind it to the iFrame element embedded in your form. In a %lt;script> element that comes after the form, place the following Javascript with your environment key:


var spreedlyPaymentFrame = new Spreedly.PaymentFrame("C7cRfNJGODKh4Iu5Ox3PToKjniY");

// Rest of PaymentFrame configuration will go in between, here

spreedlyPaymentFrame.bind(document.getElementById('spreedly-iframe'));

With this in place, you should be able to view the master form in a browser. It will look very bare and disjointed, but you should be able to see all fields, including the credit card number and CVV fields.

Submitting the form at this point will not work, since the form’s onsubmit handler is not yet defined and you have not yet registered to receive any payment-frame events.

Tokenization

The payment frame must be submitted before the master form so the payment method can be tokenized before being passed to your server environment. This sequence should be managed by the custom form onsubmit handler defined in the master form HTML.

Form onsubmit handler

Create an onsubmit handler that will be responsible for the following:

  • Using the setParam function, set additional parameters on the payment method beyond what the user has entered for the card number and CVV. Most often these values will be present in the master form. The following are required by a payment method to pass validation:
    • first_name: Card holder’s first name
    • last_name: Card holder’s last name
    • month: Card’s expiration month as a two digit number from 1-12
    • year: Card’s expiration year as a four digit number
  • Submitting the payment frame form.

This function can live anywhere as it’s defined in the global scope (for the purpose of this example).


function submitPaymentForm() {

  // These are the fields whose values we want to transfer *from* the
  // master form *to* the payment frame form.
  var paymentMethodFields = ['first_name', 'last_name', 'month', 'year']
  for(var i = 0; i < paymentMethodFields.length; i++) {
    var field = paymentMethodFields[i];
    spreedlyPaymentFrame.setParam(field, document.getElementById(field).value)
  }
  spreedlyPaymentFrame.submit();
}

Payment method event

The payment-frame library exposes a set of events that you can register for to be notified when certain milestones have occurred. This is how you will be notified that the payment method has been tokenized and you are free to submit the master form.

Register a paymentMethod callback that takes two arguments: the payment method token and the full JSON payment method representation. Within the callback set the payment_method_token field of your master form and submit the form.


spreedlyPaymentFrame.on('paymentMethod', function(token, pmData) {
  var tokenField = document.getElementById("payment_method_token");
  tokenField.setAttribute("value", token);
  var masterForm = document.getElementById('payment-form');
  masterForm.submit();
});

Try loading the master form in your browser, filling out all fields with valid values (see here for test card numbers), and pressing the submit button. The form should successfully submit the payment method to Spreedly and result in the payment method token being submitted to your server environment.

If you don’t see this behavior, open the Javascript console in your browser (often under “Developer tools”) to see any errors or further inspect the request/response to Spreedly.

Error handling

If an error occurs in the payment frame, at any point in the flow, an errors event will be triggered. To respond to errors, simply register an errors event handler that takes an array of error objects in the form:


[
  {
    "attribute":"first_name",
    "key":"errors.blank",
    "message":"First name can't be blank"
  },
  {
    "attribute":"last_name",
    "key":"errors.blank",
    "message":"Last name can't be blank"
  }
]

Errors will always have a key, which you can use to filter on, and a message which is a human readable description of the error. You can reflect errors that have an attribute in the form alongside the affected field. Errors that do not have an attribute are not field-specific.


spreedlyPaymentFrame.on('errors', function(errors) {

  // Just here for debugging. Remove this in prod
  console.log(errors);
  
  for(var i = 0; i < errors.length; i++) {
    var error = errors[i];
    if(error["attribute"]) {
      spreedlyPaymentFrame.setStyle('input#spf-' + error["attribute"], "border: 1px solid red;");
    } else {
      console.log(error["key"] + ": " + error["message"]);
    }
  }
});

With the error handler in place, submit an empty payment form. You should receive an array of errors for each invalid field. Here is a list of possible errors:

Key Description
errors.account_inactive A non-test card number was submitted against a test (unpaid) account. This can also occur when an invalid card number is submitted while testing as Spreedly assumes all unknown card numbers are real.
errors.environment_key_parameter_required An environment was not specified. Specify one when creating the Spreedly payment frame: new Spreedly.PaymentFrame("your_environment_key");
errors.invalid_environment_key_parameter The specified environment key is not valid. Check the supplied environment key value and re-submit.
errors.blank The payment method field specified by the attribute property of this error was submitted without a value, which is required by Spreedly. Display an appropriate error message and prompt the user to fill in the specified fields.
errors.invalid The payment method field specified by the attribute property of this error was submitted with an invalid value (e.g., a month value of “13”). Display an appropriate error message and prompt the user to correct the specified fields.
errors.blank_card_type The credit card number was invalid; we were unable to determine the card type.
errors.expired The payment method month and year indicates that it is expired. Display an appropriate error message and prompt the user to enter a non-expired payment method.
errors.unknown_referrer Spreedly could not determine the source (referring URL) of the form submission. A referring URL is required to identify MITM attacks.
errors.invalid_referrer The form submission did not originate from the Spreedly payment frame and has been rejected. This is a protection against MITM attempts.

Styling

The payment form that lives inside the iFrame does not have access to the styling defined for the master form in the host page. To allow you to style the payment frame, the payment-frame.js library has defined a set of calls that let you apply CSS styles, set the form labels and other tasks. These are all performed within the config callback.


spreedlyPaymentFrame.on('config', function(frame) {
  frame.setStyle('body', 'margin-left: 0px;');
  frame.setStyle('input', 'border: 1px solid #999; font-size: 12pt; font-family: Tahoma, Geneva, sans-serif;');
  frame.setStyle('label', 'font-size: 12pt; font-family: Tahoma, Geneva, sans-serif;');

  frame.setText('label[for="spf-number"]', 'Card');
  frame.setText('label[for="spf-verification_value"]', 'Cvv');

  frame.setPlaceholder('input#spf-number', 'card');
  frame.setPlaceholder('input#spf-verification_value', 'cvv');
});

The callback should accept a config variable on which you can invoke the following configuration commands:

  • setStyle(selector, styles): Apply the given CSS style to all the elements in the payment frame matching the CSS selector.
  • setText(selector, text): Set the given text value to all the elements in the payment frame matching the CSS selector. No HTML is allowed within the text and will be stripped if it’s present.
  • setPlaceholder(selector, placeholderText): Set the given placeholder text value as the placeholder attribute to all the elements in the payment frame matching the CSS selector. No HTML is allowed within the text and will be stripped if it’s present. Placeholder values applied to non-input elements will have no effect.

Since all configuration methods take a CSS selector, it is useful to know the HTML structure of the payment frame’s payment form. You can always “view source” in your browser, but here is the latest version:


<!doctype html>
<html>

<head>
  <script src="/payment-frame/frame-delegate-0.5.min.js"></script>
  <style type="text/css">
  /* Don't show the number increment/decrement spinner in number fields */
  input[type=number]::-webkit-inner-spin-button,
  input[type=number]::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
  }
  input[type='number'] {
    -moz-appearance:textfield;
  }
  </style>
</head>

<body>

  <form accept-charset="UTF-8" action="#" method="POST" id="spf-form" class="spf-form" novalidate="true">
    <div class="spf-fields">
      <span class="spf-field spf-number">
        <label id="spf-label-number" for="spf-number">Credit Card Number</label>
        <input type="number" name="credit_card[number]" id="spf-number" size="19"
        maxlength="19" autocomplete="off" aria-required="true" required>
      </span>
      <span class="spf-field spf-verification_value">
        <label id="spf-label-verification_value" for="spf-verification_value">CVV</label>
        <input type="number" name="credit_card[verification_value]" id="spf-verification_value" size="4"
        maxlength="4" autocomplete="off" aria-required="true" pattern="[0-9]*" required>
      </span>
    </div>
  </form>

  <script>
  var spreedlyFrameDelegate = new Spreedly.FrameDelegate();
  var paymentMethodForm = document.getElementById('spf-form');
  spreedlyFrameDelegate.bind(paymentMethodForm);
  </script>

</body>
</html>

Reload the master form in your browser to see the styles applied to the payment frame fields.

Validation

There are many situations where it is useful to know details of the card information the user has entered in the payment form. For instance, if you’re doing real-time validation or are displaying an icon of the card type as the user types, you need to know basic details about the user input without seeing the actual values. The payment frame library provides this functionality via the input callback which accepts a hash of metadata representing the user’s input.


spreedlyPaymentFrame.on('input', function(inputProperties) {
  if(!inputProperties["luhnValid"]) {
    spreedlyPaymentFrame.setStyle('#spf-number', 'border: red;');
  }
});

The input event is fired on every keystroke and has the following keys:

  • numberLength: The length of the user input in the card number field.
  • verificationValueLength: The length of the user input in the verification value (CVV) field.
  • cardType: The type of card entered (from this list of Spreedly supported card types). nil if the type of card could not be determined.
  • luhnValid: true if the entered card number conforms to the Luhn algorithm. false otherwise. While the Luhn algorithm does not guarantee a valid card number, it is a good way to quickly check for typos. The only way to guarantee a valid card is to run an auth or use the verify operation.

It can also be useful to operate on this input metadata before form submission. In your form onsubmit handler, invoke validate instead of submit. Then, in the validation event handler, perform your custom validation logic and, only if validation passes, submit the payment frame:


function submitPaymentForm() {

  // Set params via spreedlyPaymentFrame.setParam as before, but then
  // trigger validate to get back metadata
  spreedlyPaymentFrame.validate();
}

spreedlyPaymentFrame.on('validation', function(inputProperties) {
  if(!inputProperties["luhnValid"]) {
    spreedlyPaymentFrame.setStyle('#spf-number', 'border: red;');
  } else {
    spreedlyPaymentFrame.submit();
  }
});

Since communicating with the payment frame is asynchronous, you must operate on the input metadata in a separate validation event helper which will then become responsible for submitting the payment frame.

Display sequence

By default, we recommend setting the iFrame (or its parent) element\’s initial visibility to hidden. This allows the payment frame to be configured and styled before being displayed, which eliminates the jarring flickering effect that can occur.

When the payment frame is ready to be displayed (i.e. it has completed configuration and styling) it will trigger the ready event. Register a callback for that event to un-hide the iFrame in the host page.


spreedlyPaymentFrame.on('ready', function() {
  document.getElementById("frame-holder").style.visibility = 'visible';
});

Additional payment information

It is quite common to collect more than just the required fields when storing a payment method. If you wish to provide more detailed payment information, such as billing address, email etc…, you can use the spreedlyPaymentFrame.setParam() function to set the values of any payment method field.

For example, to add a billing address and customer email, add the following the your master form onsubmit handler.


spreedlyPaymentFrame.setParam('address1', document.getElementById('address1-field').value)
spreedlyPaymentFrame.setParam('city', document.getElementById('city-field').value)
// ... other address fields
spreedlyPaymentFrame.setParam('email', document.getElementById('email-field').value)

The following are a list of additional properties you can set using the setParam function that will be stored with the payment method:

  • full_name
  • email
  • address1
  • address2
  • city
  • state
  • zip
  • country
  • phone_number
  • company
  • shipping_address1
  • shipping_address2
  • shipping_city
  • shipping_state
  • shipping_zip
  • shipping_country
  • shipping_phone_number

Browser compatibility

payment-frame.js is compatible with the following browser versions:

  • Internet Explorer 8.0 and above
  • Google Chrome 4 and above
  • Firefox 4 and above
  • Safari 4 and above
  • iOS Safari 3.2 and above

Versioning

This version of the iFrame payment form is v0.5 and is available at:

https://core.spreedly.com/payment-frame/payment-frame-0.5.min.js

The payment-frame.js library is versioned with a major and minor numerical, e.g., 0.5.

Any critical bug fixes or security updates will be automatically and deployed as the current version number, making no action required by you to be running on a secure, stable version. For this reason, please do not store a copy of the javsacript library on your servers, always reference the version hosted on spreedly.com.

Other methods

Using a payment frame like this is the recommended way to send payment information to Spreedly. However, if this workflow doesn\’t work for you you can consider alternative methods.

Security

In order to achieve the highest levels of PCI-DSS v3 compliance with Spreedly Express or iFrame, you need to disable the creation of payment methods via the direct API. After confirming you are not using the XML, JSON, JSONP, CORS or transparent direct methods of adding a payment method, enable the “Spreedly iFrame or Express Only” option in the Environment Settings page of your environment.

This prevents direct API submission of payment methods which is required to detect malicious attacks and, in general, enforce adherence to the PCI standard.