3D Secure 2

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 five flows outlined below. Integrating with the Spreedly Test gateway will prepare your application for future integration with 3DS2 supported production gateways in the coming weeks. If you have questions not addressed in the integration guide, please contact support@spreedly.com for assistance.

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

Integration

Please use the test data below to initiate and simulate 3DS2 flows

Spreedly 3DS2 Test Data

Credit Cards

Card Number CVV Expiration Type
4556761029983886 123 10/2029 Valid 3D Secure Enrolled Card
4024007101934890 123 10/2029 Invalid 3D Secure Enrolled Card

$ Amounts

Amount in cents Result
3001 3D Secure 2 full frictionless flow (immediate transaction flow)
3002 Fallback from 3DS2 to 3DS1
3003 3D Secure device fingerprint flow with direct authorize (requires lifecycle)
3004 3D Secure device fingerprint flow to challenge (requires lifecycle and completion call)
3005 3D Secure direct challenge (requires lifecycle and completion call)
3103 3D Secure device fingerprint flow with forced failure
3104 3D Secure challenge flow with forced failure

Note: Amounts are important to simulate flows properly. If you try other amounts then you may experience unexpected behavior.

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 guide here.

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

3) 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-2.min.js"></script>
</head>

4) Generate or use an existing payment method token - follow the instructions at Spreedly API docs

5) Using the payment method token, make an authorize or purchase request - follow instructions from Spreedly API docs.

Be sure to include the following in your request, all are required:

  • attempt_3dsecure: true
  • callback_url and redirect_url used in the event that the transaction falls back to 3DS1 more info on 3DS1 here
  • three_ds_version: "2"
  • browser_info which is collected as follows:
  // Choose a browser 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 browser_size = '01';
  // 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,application/xml;q=0.9,*/*;q=0.8'
  // The request should include the browser data collected by using `Spreedly.ThreeDS.serialize().
  let browser_info = Spreedly.ThreeDS.serialize(
    browser_size,
    acceptHeader
  );

An example request to your backend will look like:

    fetch('https://your-backend.test/purchase.json', {
      method: 'POST',
      body: JSON.stringify({
        ...normalPurchaseParams,
        browser_info: browser_info,
        attempt_3dsecure: true,
        three_ds_version: "2",
      })
    });

Now, you should create an authorize or purchase request to the Spreedly API. All of the following transaction fields are required.

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

    {
      "transaction": {
        "payment_method_token": "<payment_method_token>",
        "amount": 3001,
        "currency_code": "USD",
        "redirect_url": "<redirect_url>",
        "callback_url": "<callback_url>",
        "three_ds_version": "2",
        "attempt_3dsecure": true,
        "browser_info": "<value from Spreedly.ThreeDS.serialize()>"
      }
    }

This is an example request to your backend that you’ll need to issue an authenticated purchase request to Spreedly. normalPurchaseParams will be the standard params that you pass to your backend and then on to Spreedly. They were omitted to expose what is new and required for 3DS2 requests.

Note: Parameters such as browser_info and attempt_3dsecure must match casing and underscore. browserInfo will not work.

6) Create lifecycle object to handle the following:

a. If the transaction is in an error state, handle the error and display feedback to your user

b. If the transaction is in a success state, you’re done, complete the purchase

c. If the transaction is in a pending state - Create an instance of Spreedly.ThreeDS.Lifecycle with the location for hidden iframes and payment method token

var lifecycle = new Spreedly.ThreeDS.Lifecycle({
  hiddenIframeLocation: 'device-fingerprint',
  // The DOM node that you'd like to inject hidden iframes (required)
  challengeIframeLocation: 'challenge',
  // The DOM node that you'd like to inject the challenge flow (required)
  transactionToken: '...',
  // The token for the transaction - used to poll for state (required)
  challengeIframeClasses: '...',
  // The css classes that you'd like to apply to the challenge iframe (optional)
})

7) Define lifecycle event handling

The key event types to handle are succeeded, error, challenge, and trigger-completion.

  • 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.
  • 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.
  • trigger-completion - occurs when you should make the authenticated transaction completion call from your backend to Spreedly and call event.finalize with the response data in your frontend.

Note: Worldpay uses postMessages to notify when authentication needs to progress to the next step. We pass these parameters in event.context.

   {"MessageType": "profile.completed", "SessionId": "d3197c02-6f63-4ab2-801c-83633d097e32","Status": true)

These parameters should be included in the complete call using the key context.

{context: {"MessageType": "profile.completed", "SessionId": "d3197c02-6f63-4ab2-801c-83633d097e32", "Status": true}}

These parameters will only be sent during trigger-completion events

var statusUpdates = function(event) {
  if (event.action === 'succeeded') {
    // finish your checkout and redirect to success page
  } else if (event.action === 'error') {
    // present an error to the user to retry
  } else if (event.action === 'challenge') {
    // Show your modal containing the div provided in `challengeIframeLocation` when
    // creating the lifecycle event.
    //
    // Example HTML on your page:
    //
    // <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>
    //
    //  Example lifecycle object from step 6:
    //
    //  var lifecycle = new Spreedly.ThreeDS.Lifecycle({
    //    hiddenIframeLocation: 'device-fingerprint',
    //    challengeIframeLocation: 'challenge',
    //    ...
    //  })
    //
    //  and then we show the challenge-modal
    //
    document.getElementById('challenge-modal').classList.remove('hidden');
  } else if (event.action === 'trigger-completion') {
    // 1. make a request to your backend to do an authenticated call to Spreedly to complete the request
    //    The completion call is `https://core.spreedly.com/v1/transactions/[transaction-token]/complete.json (or .xml)`
    // 2a. if the transaction is marked as "succeeded" finish your checkout and redirect to success page
    // 2b. if the transaction is marked "pending" you'll need to call finalize `event.finalize(transaction)` with the transaction data from the authenticated completion call.

    // This is an example of the authenticated call that you'd make
    // to your service.
    fetch(`https://your-service/complete/${purchaseToken}.json`, { method: 'POST' })
      .then(response => response.json())
      .then((data) => {
        if (data.state === 'succeeded') {
          // finish your checkout and redirect to success page
        }

        if (data.state === 'pending') {
          event.finalize(data);
        }
    })
  }
}

8) Setup event handling

statusUpdates is the function that we defined in step 7, it’s intended to handle all event updates that occur through the 3DS lifecycle.

Spreedly.on('3ds:status', statusUpdates)

9) Kickoff 3DSecure lifecycle

All of the following attributes are required and come from the authorize or purchase request made to Spreedly. You’ll need to return the attributes from your backend to your frontend application. In the example below you’ll see a reference to backendResponse this is an example of the data that you’ll send to your frontend from the Spreedly transaction response.

var transactionData = {
  state: backendResponse.state,
  // The current state of the transaction. 'pending', 'succeeded', etc
  required_action: backendResponse.required_action,
  // The next action to be performed in the 3D Secure workflow
  device_fingerprint_form: backendResponse.device_fingerprint_form,
  // Available when the required_action is on the device fingerprint step
  checkout_form: backendResponse.checkout_form,
  // Available when the required_action is on the 3D Secure 1.0 fallback step
  checkout_url: backendResponse.checkout_url,
  // Available when the required_action is on the 3D Secure 1.0 fallback step
  challenge_form: backendResponse.challenge_form,
  // The challenge form that is injected when the user is challenged
  challenge_url: backendResponse.challenge_url,
  // The challenge url that is loaded when there is no challenge form
};

lifecycle.start(transactionData)

Expanded 3DS2 Flow Descriptions

The 3DS2 specification introduces new transaction flows to help verify the validity of a customer with as little interruption as possible, detailed below:

1) 3DS2 full frictionless flow

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.

2) 3D Secure device fingerprint flow with direct authorize

If the initial transaction and collected browser data require more context, the fingerprint flow with direct authorize is attempted. A hidden iFrame is injected into the merchant’s page (at a specified location) and is submitted to the issuer in the background. Then, the client library will poll for up to 10 seconds, but responses are typically faster. note The Spreedly test gateway will always take 10 seconds as it is a simulated gateway to assist in preparing for 3D Secure 2.

3) 3D Secure device fingerprint flow to challenge

This occurs when the initial transaction and browser data is collected, the hidden iFrame is injected and polled, and further information is required. This is similar to 3D Secure 1.0 in the sense that a customer will be presented with an issuing bank authentication form. However, in 3DS2 the authentication form is rendered on the merchant’s site in an iFrame, typically in a modal.

4) 3D Secure direct challenge

This occurs when a transaction is deemed risky. It’s almost identical to the device fingerprint flow to challenge flow, the only difference is that the device fingerprint step is skipped.

5) 3D Secure not supported

Spreedly falls back to standard transaction processing and submits the payment.

6) Fallback from 3DS2 to 3DS1

The Spreedly Javascript will redirect the customer to an offsite page that is produced by the issuer and is then redirected back to the merchant’s site to complete the order (using the redirect_url). In the event that a transaction is not immediately successful, your application should still handle callbacks in order to be updated if the customer leaves the checkout form. See 3DS1 Callbacks for more details.

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(data)) 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.