Gateway Specific 3DS2 Guide

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. 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 Spreedly support for assistance.

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

Integration Guide for Web

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

Test Data for Spreedly 3DS2 Gateway Specific

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 Requires Lifecycle Requires Completion Endpoint
3001 3D Secure 2 full frictionless flow (immediate transaction flow)
3003 3D Secure device fingerprint flow with direct authorize Yes Yes
3004 3D Secure device fingerprint flow to challenge Yes Yes
3005 3D Secure direct challenge Yes
3103 3D Secure device fingerprint flow with forced failure Yes Yes
3104 3D Secure challenge flow with forced failure Yes

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

Integration steps

Prerequisites

  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. Generate or use an existing payment method token - follow the instructions at Spreedly API docs

Set up your front end

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>

Set up your backend

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
  • 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"
    })
  });

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.

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()>"
    }
  }

The authorize/purchase request above will return a transaction object containing a token field. Make note of this transaction.token field as you will need it for the next step: this is the transactionToken

Handle transaction response

  1. If the transaction is in an error state, handle the error and display feedback to your user. 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.

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

  3. If the transaction is in a pending state, handle the following asynchronous flow: Define/set, a) Lifecycle object to facilitate front end updating, b) Lifecycle callbacks to handle asynchronous states, c) start 3DSecure asynchronous flow

    a. Define an instance of Spreedly.ThreeDS.Lifecycle with the location for hidden iframes and payment method token

      var lifecycle = new Spreedly.ThreeDS.Lifecycle({
        environmentKey: '...',
        // The environmentKey field is required, but if omitted, you will receive a console warning message and the transaction will still succeed.
        hiddenIframeLocation: 'device-fingerprint', (required)
        // The DOM node that you'd like to inject hidden iframes
        challengeIframeLocation: 'challenge', (required)
        // The DOM node that you'd like to inject the challenge flow
        transactionToken: '...', (required)
        // The token for the transaction - used to poll for state
        challengeIframeClasses: '...', (optional)
        // The css classes that you'd like to apply to the challenge iframe.
        //
        // Note: This is where you'll change the height and width of the challenge
        //       iframe. You'll need to match the height and width option that you
        //       selected when collecting browser data with `Spreedly.ThreeDS.serialize`.
        //       For instance if you selected '04' for browserSize you'll need to have a
        //       CSS class that has width and height of 600px by 400px.
      })
    

    b. Define and set lifecycle callbacks/function for event handling called: statusUpdates

      //Define:
      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 3:
          //
          //  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);
              }
          })
        }
      }
      ...
      //Set:
      Spreedly.on('3ds:status', statusUpdates)
    

    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. This event will be fired when the transaction status was updated due to a callback in the cardholder’s iFrame or when 10 seconds have elapsed. When this event is received, it is necessary to call complete so that the latest transaction state can be retrieved in case of a timeout. After making a complete call, the frontend should make an event.finalize(data) with the response data from the api request if the transaction state returned is pending.

    finalization-timeout: occurs 10-15 minutes after presenting a challenge without the transaction state changing. It is recommended that merchants attempt a manual completion here to attempt to continue or finalize the transaction.

    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

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

    c. Start 3DSecure asynchronous flow Once it is completely setup using the above steps, the lifecycle process can be started using:

      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

Gateway Specific 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:

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 Device Fingerprint 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.

3DS2 Device Fingerprint 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. The 3DS2 authentication form is rendered on the merchant’s site in an iFrame, typically in a modal.

3DS2 Denied

If Authentication fails, is rejected or denied, the transaction will be marked as failed and it is up to the merchant to retry the transaction or deny it.

3DS Not Enrolled/Supported

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

Redirects

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 3DS2 Callbacks for more details.

Callbacks

In a best case scenario, a customer’s browser will always come back to your site after a successful payment, but in the real world, there are various reasons the redirect might not occur as designed. The callback_url you specified when creating the purchase provides another way to receive notice of transaction state.

The callback url will receive a POST of all transactions that have changed since the last callback. In most cases, you’ll receive the redirect and a callback, and the order of the two is not guaranteed. Generally, you’ll just want to pay attention to the first one and ignore the second one, since you’ve already handled the transaction.

In some cases, such as a Paypal eCheck, the transaction initially moved to a state of processing and stayed there for a few days before the funds were actually transferred. You’ll eventually receive a callback indicating that the transaction has moved to a different state. Here are the possible states for a transaction:

  • succeeded: The transaction has succeeded and funds have been received.
  • processing: The transaction has been accepted. Funds have not yet been received.
  • pending: The transaction needs further processing which typically involves redirecting the customer to a redirect_url to allow them to specify a payment method.
  • gateway_processing_failed: The transaction failed because the gateway declined the charge for some reason.
  • gateway_processing_result_unknown: We had difficulty communicating with the service and we’re unsure what the result of the operation was. (timeouts, connection errors, etc).
  • failed: The transaction failed. This could be caused by a number of things such as the payment method not being valid, the payment method being redacted, etc.
  • gateway_setup_failed: The transaction failed because the attempt to setup the transaction on the offsite gateway failed.

The callback POST looks like this:

<transactions>
  <transaction>
    <on_test_gateway type="boolean">false</on_test_gateway>
    <created_at type="dateTime">2015-01-08T15:31:32-05:00</created_at>
    <updated_at type="dateTime">2015-01-08T15:31:32-05:00</updated_at>
    <succeeded type="boolean">true</succeeded>
    <state>succeeded</state>
    <token>APIKmsDwoRKre5QNz5UrC02eOen</token>
    <transaction_type>OffsiteAuthorization</transaction_type>
    <order_id nil="true"/>
    <ip nil="true"/>
    <description nil="true"/>
    <email nil="true"/>
    <merchant_name_descriptor nil="true"/>
    <merchant_location_descriptor nil="true"/>
    <gateway_specific_fields nil="true"/>
    <gateway_specific_response_fields nil="true"/>
    <gateway_transaction_id nil="true"/>
    <amount type="integer">1400</amount>
    <currency_code>USD</currency_code>
    <reference>TheReferenceId</reference>
    <message key="messages.transaction_succeeded">Succeeded!</message>
    <gateway_token>8XJtbE1p4NTZ6fFqwwn0GrkjEmW</gateway_token>
    <response>
      <success type="boolean">true</success>
      <message>Succeeded</message>
      <avs_code nil="true"/>
      <avs_message nil="true"/>
      <cvv_code nil="true"/>
      <cvv_message nil="true"/>
      <pending type="boolean">false</pending>
      <result_unknown type="boolean">false</result_unknown>
      <error_code nil="true"/>
      <error_detail nil="true"/>
      <cancelled nil="true"/>
      <fraud_review type="boolean">false</fraud_review>
      <created_at type="dateTime">2015-01-08T15:31:32-05:00</created_at>
      <updated_at type="dateTime">2015-01-08T15:31:32-05:00</updated_at>
    </response>
    <api_urls>
      <callback_conversations>http://core.spreedly.dev/v1/callbacks/XjCHPJad2pDbIKKq2fOJezXmxiM/conversations.xml</callback_conversations>
    </api_urls>
    <signed>
      <signature>81c296289369d1331e17fc5a8c82451282fc0889</signature>
      <fields>amount created_at currency_code ip on_test_gateway order_id state succeeded token transaction_type updated_at</fields>
      <algorithm>sha1</algorithm>
    </signed>
    <payment_method>
      <token>UAUT1ArRmsXXuXdzS3XbALyYgNr</token>
      <created_at type="dateTime">2015-01-08T15:31:32-05:00</created_at>
      <updated_at type="dateTime">2015-01-08T15:31:32-05:00</updated_at>
      <email nil="true"/>
      <data>
        <email>gandalf@wizards.com</email>
      </data>
      <storage_state>retained</storage_state>
      <test type="boolean">true</test>
      <payment_method_type>sprel</payment_method_type>
      <errors>
      </errors>
    </payment_method>
    <callback_url>https://127.0.0.1</callback_url>
    <redirect_url>http://example.com/handle_redirect</redirect_url>
    <checkout_url>http://core.spreedly.dev/sprel/8XJtbE1p4NTZ6fFqwwn0GrkjEmW/checkout/APIKmsDwoRKre5QNz5UrC02eOen</checkout_url>
    <checkout_form>
      <![CDATA[
<form action="" method="POST">
  <div>
    <input name="PaReq" value="" type="hidden"/>
    <input name="MD" value="" type="hidden"/>
    <input name="TermUrl" value="http://core.spreedly.dev/transaction/APIKmsDwoRKre5QNz5UrC02eOen/redirect" type="hidden"/>
    <input name="Complete" value="Authorize Transaction" type="submit"/>
  </div>
</form>
]]>
    </checkout_form>
    <setup_response>
      <success type="boolean">true</success>
      <message nil="true"/>
      <error_code nil="true"/>
      <checkout_url>http://core.spreedly.dev/sprel/8XJtbE1p4NTZ6fFqwwn0GrkjEmW/checkout/APIKmsDwoRKre5QNz5UrC02eOen</checkout_url>
      <created_at type="dateTime">2015-01-08T15:31:32-05:00</created_at>
      <updated_at type="dateTime">2015-01-08T15:31:32-05:00</updated_at>
    </setup_response>
    <redirect_response>
      <success type="boolean">true</success>
      <message>Succeeded</message>
      <avs_code nil="true"/>
      <avs_message nil="true"/>
      <cvv_code nil="true"/>
      <cvv_message nil="true"/>
      <pending type="boolean">false</pending>
      <result_unknown type="boolean">false</result_unknown>
      <error_code nil="true"/>
      <error_detail nil="true"/>
      <cancelled nil="true"/>
      <fraud_review type="boolean">false</fraud_review>
      <created_at type="dateTime">2015-01-08T15:31:32-05:00</created_at>
      <updated_at type="dateTime">2015-01-08T15:31:32-05:00</updated_at>
    </redirect_response>
  </transaction>
  <transaction>
    <on_test_gateway type="boolean">false</on_test_gateway>
    <created_at type="dateTime">2015-01-08T15:55:02-05:00</created_at>
    <updated_at type="dateTime">2015-01-08T15:55:02-05:00</updated_at>
    <succeeded type="boolean">true</succeeded>
    <state>succeeded</state>
    <token>2tLQS3ebpsCBYvtNtgc2kXXXkL</token>
    <transaction_type>Purchase</transaction_type>
    <order_id nil="true"/>
    <ip nil="true"/>
    <description nil="true"/>
    <email nil="true"/>
    <merchant_name_descriptor nil="true"/>
    <merchant_location_descriptor nil="true"/>
    <gateway_specific_fields nil="true"/>
    <gateway_specific_response_fields nil="true"/>
    <gateway_transaction_id nil="true"/>
    <amount type="integer">100</amount>
    <currency_code>USD</currency_code>
    <retain_on_success type="boolean">false</retain_on_success>
    <payment_method_added type="boolean">false</payment_method_added>
    <message key="messages.transaction_succeeded">Succeeded!</message>
    <gateway_token>8XJtbE1p4NTZ6fFqwwn0GrkjEmW</gateway_token>
    <response>
      <success type="boolean">true</success>
      <message>Succeeded</message>
      <avs_code nil="true"/>
      <avs_message nil="true"/>
      <cvv_code nil="true"/>
      <cvv_message nil="true"/>
      <pending type="boolean">false</pending>
      <result_unknown type="boolean">false</result_unknown>
      <error_code nil="true"/>
      <error_detail nil="true"/>
      <cancelled type="boolean">false</cancelled>
      <fraud_review type="boolean">false</fraud_review>
      <created_at type="dateTime">2015-01-08T15:55:02-05:00</created_at>
      <updated_at type="dateTime">2015-01-08T15:55:02-05:00</updated_at>
    </response>
    <api_urls>
      <callback_conversations>http://core.spreedly.dev/v1/callbacks/XjCHPJad2pDbIKKq2fOJezXmxiM/conversations.xml</callback_conversations>
    </api_urls>
    <signed>
      <signature>4c18641228a9415318d9f89cee64c0f26f075eaa</signature>
      <fields>amount created_at currency_code ip on_test_gateway order_id state succeeded token transaction_type updated_at</fields>
      <algorithm>sha1</algorithm>
    </signed>
    <payment_method>
      <token>IPhWm8DU6sKOODBQnXb087f9yIA</token>
      <created_at type="dateTime">2015-01-08T15:55:02-05:00</created_at>
      <updated_at type="dateTime">2015-01-08T15:55:02-05:00</updated_at>
      <email nil="true"/>
      <data nil="true"/>
      <storage_state>used</storage_state>
      <test type="boolean">true</test>
      <last_four_digits>3886</last_four_digits>
      <first_six_digits>455676</first_six_digits>
      <card_type>visa</card_type>
      <first_name>Bob</first_name>
      <last_name>Smith</last_name>
      <month type="integer">2</month>
      <year type="integer">2020</year>
      <address1 nil="true"/>
      <address2 nil="true"/>
      <city nil="true"/>
      <state nil="true"/>
      <zip nil="true"/>
      <country nil="true"/>
      <phone_number nil="true"/>
      <full_name>Bob Smith</full_name>
      <eligible_for_card_updater type="boolean">true</eligible_for_card_updater>
      <shipping_address1 nil="true"/>
      <shipping_address2 nil="true"/>
      <shipping_city nil="true"/>
      <shipping_state nil="true"/>
      <shipping_zip nil="true"/>
      <shipping_country nil="true"/>
      <shipping_phone_number nil="true"/>
      <payment_method_type>credit_card</payment_method_type>
      <errors>
      </errors>
      <verification_value></verification_value>
      <number>XXXX-XXXX-XXXX-3886</number>
    </payment_method>
    <callback_url>https://127.0.0.1</callback_url>
    <redirect_url>http://example.com/handle_redirect</redirect_url>
    <checkout_url nil="true"/>
    <checkout_form>
      <![CDATA[
<form action="http://core.spreedly.dev/test/8XJtbE1p4NTZ6fFqwwn0GrkjEmW/auth/2tLQS3ebpsCBYvtNtgc2kXXXkL" method="POST">
  <div>
    <input name="PaReq" value="" type="hidden"/>
    <input name="MD" value="" type="hidden"/>
    <input name="TermUrl" value="http://core.spreedly.dev/transaction/2tLQS3ebpsCBYvtNtgc2kXXXkL/redirect" type="hidden"/>
    <input name="Complete" value="Authorize Transaction" type="submit"/>
  </div>
</form>
]]>
    </checkout_form>
    <setup_response>
      <success type="boolean">true</success>
      <message>Checked enrollment status</message>
      <error_code></error_code>
      <checkout_url nil="true"/>
      <created_at type="dateTime">2015-01-08T15:55:02-05:00</created_at>
      <updated_at type="dateTime">2015-01-08T15:55:02-05:00</updated_at>
    </setup_response>
    <redirect_response>
      <success type="boolean">true</success>
      <message>Succeeded</message>
      <avs_code nil="true"/>
      <avs_message nil="true"/>
      <cvv_code nil="true"/>
      <cvv_message nil="true"/>
      <pending type="boolean">false</pending>
      <result_unknown type="boolean">false</result_unknown>
      <error_code nil="true"/>
      <error_detail nil="true"/>
      <cancelled type="boolean">false</cancelled>
      <fraud_review type="boolean">false</fraud_review>
      <created_at type="dateTime">2015-01-08T15:55:02-05:00</created_at>
      <updated_at type="dateTime">2015-01-08T15:55:02-05:00</updated_at>
    </redirect_response>
  </transaction>
</transactions>

Note that the callback response is signed; this allows you to process the results of the callback without having to round trip back out to Spreedly (though you certainly can round trip if you’d like - see below). Since an attacker could call your callback url with a valid looking transaction, the signature allows you to verify that the information is really coming from Spreedly. Full details on validating the signature is in our Signing Reference.

We recognize that some customers may not be interested in going through the trouble of writing code to validate the signature of the callback response. In this case, you could simply grab the tokens of the transactions you receive in the callback and then make an authenticated API call to retrieve the details of each transaction. Because that call is authenticated and you’re making the request, there’s no need to verify where the information is coming from.

Callback Response

You must respond to the callback with a 200 OK response within 5 seconds. If Spreedly does not receive a 200 response within this time, it will retry the callback again at least 4 times at increasing intervals.

If you are sending all transaction callbacks to the same callback url, new transactions will also be limited by the increasing retry intervals. The transaction callbacks associated with the same callback url will fire one at a time, in order, as long as the url continues to respond correctly.

If you need to do potentially time-consuming operations when a callback is received, we recommend doing them asynchronously to avoid being timed out.

Error handling

Of course, things don’t go exactly right all the time. And with offsite transactions, there are more places that things can break, since there are more interactions with the gateway. The state element will change to reflect failures when processing:

curl https://core.spreedly.com/v1/transactions/TC7i2O0mhzpes1BaCaIdzGzHQuL.xml \
  -u 'Ll6fAtoVSTyVMlJEmtpoJV8S:RKOCG5D8D3fZxDSg504D0IxU2XD4Io5VXmyzdCtTivHFTTSy'

In general you can just depend on the message element when something goes wrong - we do our best to provide a useful message there that you can display rather than having to dig around in the transaction details. That said, up to three responses are returned with the transaction: setup_response, redirect_response, and callback_response. These may have additional error details that will aid in debugging any issues.

The transaction’s state can also be an indication as to what has failed. For example, when initiating the purchase, we setup a transaction on the offsite gateway. If that setup process fails, the state of the transaction will be gateway_setup_failed. (You can simulate this with the Test gateway by specifying an amount of 44 cents).

Like always, you can use the transcript call to get a verbatim copy of Spreedly’s actual conversation with the gateway.

<transaction>
  <on_test_gateway type="boolean">false</on_test_gateway>
  <created_at type="dateTime">2015-01-08T16:03:43-05:00</created_at>
  <updated_at type="dateTime">2015-01-08T16:03:49-05:00</updated_at>
  <succeeded type="boolean">true</succeeded>
  <state>succeeded</state>
  <token>SrkEQGNZSmjJYQTEepl5de7Wc7M</token>
  <transaction_type>Purchase</transaction_type>
  <order_id nil="true"/>
  <ip nil="true"/>
  <description nil="true"/>
  <email nil="true"/>
  <merchant_name_descriptor nil="true"/>
  <merchant_location_descriptor nil="true"/>
  <gateway_specific_fields nil="true"/>
  <gateway_specific_response_fields nil="true"/>
  <gateway_transaction_id nil="true"/>
  <amount type="integer">100</amount>
  <currency_code>USD</currency_code>
  <retain_on_success type="boolean">false</retain_on_success>
  <payment_method_added type="boolean">false</payment_method_added>
  <message key="messages.transaction_succeeded">Succeeded!</message>
  <gateway_token>8XJtbE1p4NTZ6fFqwwn0GrkjEmW</gateway_token>
  <response>
    <success type="boolean">true</success>
    <message>Succeeded</message>
    <avs_code nil="true"/>
    <avs_message nil="true"/>
    <cvv_code nil="true"/>
    <cvv_message nil="true"/>
    <pending type="boolean">false</pending>
    <result_unknown type="boolean">false</result_unknown>
    <error_code nil="true"/>
    <error_detail nil="true"/>
    <cancelled type="boolean">false</cancelled>
    <fraud_review type="boolean">false</fraud_review>
    <created_at type="dateTime">2015-01-08T16:03:43-05:00</created_at>
    <updated_at type="dateTime">2015-01-08T16:03:49-05:00</updated_at>
  </response>
  <api_urls>
    <callback_conversations>http://core.spreedly.dev/v1/callbacks/XjCHPJad2pDbIKKq2fOJezXmxiM/conversations.xml</callback_conversations>
  </api_urls>
  <payment_method>
    <token>OyJJXGWSaManB7NhahLTsceOFi</token>
    <created_at type="dateTime">2015-01-08T16:03:42-05:00</created_at>
    <updated_at type="dateTime">2015-01-08T16:03:43-05:00</updated_at>
    <email nil="true"/>
    <data nil="true"/>
    <storage_state>used</storage_state>
    <test type="boolean">true</test>
    <last_four_digits>3886</last_four_digits>
    <first_six_digits>455676</first_six_digits>
    <card_type>visa</card_type>
    <first_name>Bob</first_name>
    <last_name>Smith</last_name>
    <month type="integer">2</month>
    <year type="integer">2020</year>
    <address1 nil="true"/>
    <address2 nil="true"/>
    <city nil="true"/>
    <state nil="true"/>
    <zip nil="true"/>
    <country nil="true"/>
    <phone_number nil="true"/>
    <full_name>Bob Smith</full_name>
    <eligible_for_card_updater type="boolean">true</eligible_for_card_updater>
    <shipping_address1 nil="true"/>
    <shipping_address2 nil="true"/>
    <shipping_city nil="true"/>
    <shipping_state nil="true"/>
    <shipping_zip nil="true"/>
    <shipping_country nil="true"/>
    <shipping_phone_number nil="true"/>
    <payment_method_type>credit_card</payment_method_type>
    <errors>
    </errors>
    <verification_value></verification_value>
    <number>XXXX-XXXX-XXXX-3886</number>
  </payment_method>
  <callback_url>https://127.0.0.1</callback_url>
  <redirect_url>http://example.com/handle_redirect</redirect_url>
  <checkout_url nil="true"/>
  <checkout_form>
    <![CDATA[
<form action="http://core.spreedly.dev/test/8XJtbE1p4NTZ6fFqwwn0GrkjEmW/auth/SrkEQGNZSmjJYQTEepl5de7Wc7M" method="POST">
  <div>
    <input name="PaReq" value="" type="hidden"/>
    <input name="MD" value="" type="hidden"/>
    <input name="TermUrl" value="http://core.spreedly.dev/transaction/SrkEQGNZSmjJYQTEepl5de7Wc7M/redirect" type="hidden"/>
    <input name="Complete" value="Authorize Transaction" type="submit"/>
  </div>
</form>
]]>
  </checkout_form>
  <setup_response>
    <success type="boolean">true</success>
    <message>Checked enrollment status</message>
    <error_code></error_code>
    <checkout_url nil="true"/>
    <created_at type="dateTime">2015-01-08T16:03:43-05:00</created_at>
    <updated_at type="dateTime">2015-01-08T16:03:49-05:00</updated_at>
  </setup_response>
  <redirect_response>
    <success type="boolean">true</success>
    <message>Succeeded</message>
    <avs_code nil="true"/>
    <avs_message nil="true"/>
    <cvv_code nil="true"/>
    <cvv_message nil="true"/>
    <pending type="boolean">false</pending>
    <result_unknown type="boolean">false</result_unknown>
    <error_code nil="true"/>
    <error_detail nil="true"/>
    <cancelled type="boolean">false</cancelled>
    <fraud_review type="boolean">false</fraud_review>
    <created_at type="dateTime">2015-01-08T16:03:43-05:00</created_at>
    <updated_at type="dateTime">2015-01-08T16:03:49-05:00</updated_at>
  </redirect_response>
</transaction>

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.