Spreedly 3DS2 Global Integration Guide for Web

3D Secure is the name for a protocol established by Visa and MasterCard that adds two-factor authentication to credit card transactions on those networks. While avoided by merchants whenever possible (since it adds a huge barrier to customers completing a transaction), and basically unused in the US, it is sometimes a requirement for some non-US based merchants.

Two things are important to keep in mind about the 3D Secure flow on Spreedly:

  1. 3D Secure is completely optional, even for gateways that support it. You must flag transactions (details below) in order for Spreedly to even attempt 3D secure on them. Similarly, even if a transaction is flagged, it will be processed regardless if run on a gateway that doesn’t support 3D Secure.
  2. You must support our asynchronous transaction flow (also used for Offsite Payments, details below) in order for it to work. It’s as similar as possible to the normal workflow, but you can’t just “flip a switch” and have it start working.

To prepare your payment application for the Payment Services Directive and 3DS2 Requirements with Spreedly, you will need to implement and test each of the flows outlined below. If you have questions not addressed in the integration guide, please contact Spreedly support for assistance.

For general PSD2 Compliance information, please visit our PSD2 Compliance Guide.

Prerequisites

Before starting, make sure you have completed your Merchant Profile and SCA Provider set up using our Spreedly 3DS2 Global guide

  1. You will need a Spreedly Test gateway token to interact with the simulated 3DS2 flows. If you already have a Spreedly Test gateway setup you are likely good to go. If you don’t have a Spreedly Test gateway setup or want to spin up a new one for 3DS2 testing, follow the testing guide.

    Note: Alternatively, you may test against a specific gateway by creating one in sandbox mode.

  2. Verify your test gateway can successfully make a purchase without 3DS. This will help limit future troubleshooting to 3DS specific changes.

Guide companion sample app

Your app probably uses a framework (e.g. Angular, React, Vue, etc) or custom project layout; the interaction between your backend and frontend, and handling of our events will vary, depending on which.

We’ve created a sample Spreedly 3DS Reference Implementation which is a pure HTML/JS/CSS app that illustrates all the concepts laid out in this guide, simulating 2 simple checkout experiences using our iFrame and Express API.

Prepare Your App

Spreedly provides helpers in our Express and iFrame libraries for handling asynchronous 3DS2 transactions in the browser. The method is the same as it was with Gateway Specific 3DS2 integrations, so if you have integrated with any of those then you should be ready to start processing transactions with a Spreedly SCA Provider. If not, below is a guide to using the 3DS functionality of our iFrame and Express libraries.

  1. Include Spreedly iFrame javascript on your checkout page

    <head>
      <script src="https://core.spreedly.com/iframe/iframe-v1.min.js"></script>
    </head>
    

    or include Spreedly Express javascript

    <head>
      <script src="https://core.spreedly.com/iframe/express-3.min.js"></script>
    </head>
    
  2. Generate or use an existing payment method token. e.g.: using our iFrame, Express or API calls

    More details available at Spreedly API Payment Method

Start the Transaction

In addition to the standard information required for a non-3DS2 transaction, additional browser information is required to help assess the risk of a given transaction. Spreedly’s iFrame and Express libraries provide a helper function that captures this information and allows you to relay it to our backend as part of the gateway transaction.

  // Choose a challenge window size for your application.
  // This will be the size of the challenge iframe that will be presented
  // to a user.
  //
  // Note: If you're creating a modal, you should make the surrounding DOM node a
  // little larger than the option selected below.
  //
  // '01' - 250px x 400px
  // '02' - 390px x 300px
  // '03' - 500px x 600px
  // '04' - 600px x 400px
  // '05' - fullscreen
  var challenge_window_size = '04';

  // The accept header from your server side rendered page.
  // You'll need to inject it into the page. Below is an example.
  var acceptHeader = 'text/html,application/xhtml+xml;q=0.9,*/*;q=0.8'

  // Capture browser data by using `Spreedly.ThreeDS.serialize().
  let browser_info = Spreedly.ThreeDS.serialize(
    challenge_window_size,
    acceptHeader
  );

An example request to your backend API may look like:

  fetch('https://your-backend.test/do-purchase.json', {
    method: 'POST',
    body: JSON.stringify({
      your_param1: 'your_value1',
      your_param2: 'your_value2',
      // ... more params
      your_paramN: 'your_value1'
    })
  });

In the example, your_param[1..N] will be the parameters to the backend, that you control and define, e.g.:

  • order_id, or something that helps you determine what amount to pass.
  • token, may represent the value for our payment_method_token below
  • client_browser, our browser_info, generated above

In your backend, create an authorize or purchase request to the Spreedly API.

  POST /v1/gateways/<gateway_token>/purchase.json HTTPS/1.1
  Host: core.spreedly.com
  Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
  Content-Type: application/<format>

  {
    "transaction": {
      "sca_provider_key": "<sca_provider_key>",
      "payment_method_token": "<payment_method_token>",
      "amount": 10000,
      "currency_code": "EUR",
      "callback_url": "<callback_url (optional)>",
      "browser_info": "<value from Spreedly.ThreeDS.serialize()>"
    }
  }

The communication between your frontend and backend is entirely up to you, we recommend you keep track of the value used in payment_method_token value.

A quick and easy way to do this is passing back your frontend the token from the Spreedly API call you made, as part of your own response. For details see our Spreedly API purchase docs. After the purchase or authorize call, there are three possible scenarios you will need to handle:

  • If response.state == "failed", handle the error response and display feedback to your user.
  • If response.state == "succeeded", the purchase has completed and you can direct your user to a confirmation page.
  • If response.state == "pending", then a challenge is required to finish the authentication. Please see the following section on how to finish a pending transaction.

Let Spreedly 3DS handle your customer authentication

If the transaction is in a pending state, the Spreedly Lifecycle object can be used to help simplify your frontend integration. The Lifecycle object polls the Spreedly backend and emits events when the transaction changes status or times out. If a challenge needs to be displayed, it will create an iFrame element and inject it with the form from the acquiring bank. A Lifecycle object needs the following parameters during initialization:

var lifecycle = new Spreedly.ThreeDS.Lifecycle({
  // The environmentKey field is highly recommended,
  // if omitted lifecycle may miss certain update events
  // but the transaction will still succeed.
  environmentKey: '...',

  // The DOM node that you'd like to inject hidden iframes
  hiddenIframeLocation: 'device-fingerprint', // (required)

  // The DOM node that you'd like to inject the challenge flow
  challengeIframeLocation: 'challenge', // (required)

  // The token for the transaction - used to poll for state
  transactionToken: '...', // (required)

  // The css classes that you'd like to apply to the challenge iframe.
  // e.g. 'red-border left-positioned custom-styles'
  challengeIframeClasses: '...', // (optional)
})
  <head>
    <style>
      .hidden {
        display: none;
      }
      #challenge-modal {
        /* style your modal here */
      }
    </style>
  </head>
  <body>
    <div id="device-fingerprint" class="hidden">
      <!-- Spreedly injects content into this div,
      do not nest the challenge div inside of it -->
    </div>
    <div id="challenge-modal" class="hidden">
      <div id="challenge"></div>
    </div>
  </body>

Lifecycle allows users to attach event handlers that will receive updates about a transaction via events. In your callback function, the following event.action values should be handled:

  • succeeded - occurs when the transaction has finished and it’s time to move your user away from your checkout page.
  • error - occurs when there was an error with the transaction and you should either present an error to the user or cancel the transaction. Transactions that have failed cannot be updated or used to challenge the cardholder again; if you would like to present a cardholder with a new challenge upon failure, a new transaction should be used.
  • challenge - occurs when it’s time to pop open the challenge flow. It’s recommended that you put the challengeIframeLocation inside of another containing DIV that is hidden and show it at this time.
  • finalization-timeout - your customer authentication could not be completed within the expected window. This gets triggered 10-15 minutes after presenting a challenge without the transaction state changing. See our Flow Descriptions for more details.

To attach an event handler, declare it as a function and use the Spreedly.on function as shown below:

  var on3DSstatusUpdatesFn = function(threeDsStatusEvent) {
    if (threeDsStatusEvent.action === 'succeeded') {

      // finish your checkout and redirect to success page

    } else if (threeDsStatusEvent.action === 'error') {
      // present an error to the user to retry
    } else if (threeDsStatusEvent.action === 'finalization-timeout') {
      // present an error to the user to retry
    } else if (threeDsStatusEvent.action === 'challenge') {
      // show the challenge-modal
      document.getElementById('challenge-modal').classList.remove('hidden');
    }
  }

  // Setup event handling and kickoff 3DSecure lifecycle
  Spreedly.on('3ds:status', on3DSstatusUpdatesFn)

Finally, start the polling process by calling the Lifecycle’s start function:

lifecycle.start()

Note: If you plan to run multiple 3DS2 authentications without a full page reload or redirect:

  • The Spreedly.on('3ds:status', statusUpdates) function should only be invoked once. Invoking it multiple times will register the event handlers multiple times leading to duplicate events being received.
  • Make sure to call lifecycle.start() as soon as possible after receiving a pending status for a given transaction, as there is a 30 second timeout between the authentication response and challenge load specified as part of the 3DS V2 spec, see EMVco Spec 2.2.0, 5.5

End to end flow diagram

Spreedly 3DS2 Global Flow Descriptions

Detailed below are the flows possible for a Spreedly 3DS2 Global transaction. Please note the behavior can differ from Gateway Specific 3DS flows.

3DS2 Fully Frictionless

This flow represents the smoothest path through to transaction success. During the authorize or purchase flow the transaction, along with collected browser data, is deemed enough to verify the purchaser. No further action is required.

3DS2 Direct Challenge

This occurs when a transaction is deemed risky. The customer will be presented with an authentication form from the issuing bank, rendered in an iFrame, typically a modal.

3DS2 Denied

If Authentication fails, is rejected or denied, Spreedly fails the transaction and no call is made to the gateway.

3DS Not Enrolled/Supported

Spreedly fails the transaction and no call is made to the gateway. You may wish to re-attempt the transaction without requesting Spreedly 3DS2.

Troubleshooting

  1. For security reasons you should never make api requests from your frontend application directly to Spreedly. For this reason the authorize/purchase and complete transaction requests need to be made from your backend application.
  2. Ensure that the Spreedly Javascript library has loaded properly. You can do so by looking at your browser developer tools and looking at the network traffic.
  3. Ensure that your data in the purchase and completion requests is being passed to the Javascript library correctly. console.dir the object that you’re passing to the javascript library right after the purchase request (before lifecycle.start()) and before event.finalize.
  4. Ensure that you’re collecting the accept header from your server side rendered page correctly (console.log). It might be best to inject the accept header as a hidden form field and grab it with javascript.
  5. Ensure that the ordering of interactions with the Spreedly Javascript library are correct. Create a function to listen to events, then register that event handler with Spreedly and issue your purchase request (with browser info). Finally, issue start on the lifecycle object you’ve created.
  6. You can also check where things are at by doing a console.dir on the event object in the statusUpdate event handler.
  7. If all else fails, please reach out! Contact us at support@spreedly.com - we’d love to help.

Want to learn more about 3DS2?

You can read more about the regulations behind these changes on our blog or see the full specification details at the source, EMVco.

FAQs

See a list of all 3DS frequently asked questions in the Help Center.