Spreedly 3DS2 Global Integration Guide for Mobile Webview

Spreedly 3DS2 Global is currently in beta. Documentation and API usage are subject to change before final release. Please contact Spreedly support if you encounter any problems with your Spreedly 3DS2 Global integration.
For users who want to integrate with our 3DS2 Gateway Specific solution for mobile, see this guide

To prepare your mobile 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.

If you have already implemented 3DS2 with Spreedly using the Spreedly 3DS2 Global Guide you will likely recognize many of these steps. However, it is important to note that this is a separate integration requiring the code blocks below to be integrated into your native mobile application and rendered in a webview and as such, all events from the lifecycle object should post messages to your native application.

What you’ll need:

  • A strong working knowledge of mobile development
  • A native mobile application to test out the webview approach
  • A web application where you can host HTML files on your companies domain. All HTML templates below are recommended to be hosted on your domain.


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. Load Spreedly iFrame javascript in a webview

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

    or load Spreedly Express javascript in a webview

      <script src="https://core.spreedly.com/iframe/express-3.min.js"></script>
  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.

  <!doctype html>

  <html lang="en">
      <!-- load spreedly iframe -->
      <script src="https://core.spreedly.com/iframe/iframe-v1.min.js"></script>

    <script type="text/javascript">
      (function() {
        var challengeWindowSize = "05";
        var browserInfo = Spreedly.ThreeDS.serialize(challengeWindowSize);
        // Post a message to your native application. postMessageToNativeApp is an
        // example function. Depending on platform IOS, Android, etc you'll need to
        // replace it with a platform specific call.

and in your native mobile code you’ll accept the message posted with browser info and then make your purchase request

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. Note: The callback(URL) is an additional, optional way of receiving notification directly from our server to your backend. You can find more information about callbacks in our 3DS1 guide. 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 required, but if omitted,
  // you will receive a console warning message and 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)
      .hidden {
        display: none;
      #challenge-modal {
        /* style your modal here */
    <div id="device-fingerprint" class="hidden">
      <!-- Spreedly injects content into this div,
      do not nest the challenge div inside of it -->
    <div id="challenge-modal" class="hidden">
      <div id="challenge"></div>

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') {

      // post message to mobile app that the transaction was successful and finish your checkout

    } 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

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

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


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.

Fallback from 3DS2 to 3DS1

The Spreedly Javascript will present a challenge form to the user within the browser window, just as with a regular challenge, but the version of 3DS will resolve to v1.


  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.


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