PayPal Offsite Payments

A payment type such as PayPal requires that a customer navigate “offsite” to a gateway supplied page to fill in payment details or select a payment method stored at the gateway in order for Spreedly to complete the payment process. Spreedly calls these “offsite” flows since it does require sending/redirecting the customer off of your site in order for the transaction to be completed.

Spreedly has followed, as much as possible, the traditional payment flow of other supported gateways but there are important differences and additional complexity that must be handled for this offsite process.

Note: If you are currently using the legacy PayPal integration, please review the prior guide here.

How PayPal and Spreedly offsite payments work

The flow for initiating the payment process with Spreedly for a PayPal payment is very similar to that of a standard credit card payment. It starts with a Spreedly payment method.

The payment method creation will be initiated by submitting to Spreedly’s /payment_methods endpoint and specifying a payment_method_type of PayPal. Spreedly responds with a new payment method token and will redirect back to the merchant backend server for the next step in the flow.

Once a payment method has been created, the merchant can elect to perform Spreedly’s authorize() or purchase(). In this example, an authorize() will be initiated:

This is where the flow deviates from the normal Spreedly workflow for processing a payment. At the time an /authorize call is made, Spreedly will initiate a request to PayPal. In response, PayPal provides Spreedly a checkout URL and Spreedly, in turn, provides this to the merchant backend service that initiated the authorize call.

At this time, the merchant must then redirect their customer to that PayPal checkout URL to authenticate and select a payment method type as PayPal:

After a user selects a payment method type at PayPal, they will then be redirected back to the merchant site (this is a configurable URL specified by the merchant via Spreedly’s API). In addition to the redirection back to the merchant site, Spreedly also updates the authorization status and completes a subsequent call to PayPal to finalize the authorization process.

Once a successful authorization has occurred, it is up to the the merchant when to perform the capture. At this point, it is just follows the standard Spreedly flow and a capture call is initiated referencing the prior authorization:

Where to Start with Spreedly and PayPal?

Create a Gateway

If you are new to PayPal, the best place to start is by registering for a PayPal developer account here. Once a developer account is established, follow PayPal’s documentation and setting up a new REST application. You will need the appropriate Client ID and Secret in order to setup a new Spreedly gateway configuration.

If you are not ready to create a new account at PayPal, the Spreedly test gateway can simulate offsite flow. To create a Spreedly test gateway, see here. The rest of the steps below will simulate the basic offsite flow required to build into your application.

Create Payment Form

After creating a gateway that supports offsite purchases you need to collect payment information using a transparent redirect form, just like regular credit card transactions. The difference is you aren’t capturing payment detail such as credit card number, address, etc. If you are doing just a one-time purchase, you can create a simple form as seen below to create a payment method with a payment_method_type = paypal. In this example, we’re going to use the test gateway’s fake offsite sprel payment type (in production use values such as: paypal):

Spreedly does not use the traditional PayPal Smart Buttons to initiate transaction with PayPal. The initiation of the transaction starts by creating a new payment method within Spreedly via a form post to the Spreedly API payment_methods endpoint. You may stylize the form however you like, including a graphic of the PayPal button (speak with your PayPal account manager on guidance) but all the interaction will be directly with Spreedly.

<form accept-charset="UTF-8" action="https://core.spreedly.com/v1/payment_methods" method="POST">
  <input name="redirect_url" type="hidden" value="http://example.com/transparent_redirect_complete" />
  <input name="environment_key" type="hidden" value="C7cRfNJGODKh4Iu5Ox3PToKjniY" />
  <input name="utf8" type="hidden" value="✓" />
  <fieldset>
    <button type="submit" name="payment_method_type" value="sprel">Pay via Sprel</button>
  </fieldset>
</form>

Note: Ensure that you replace redirect_url and environment_key with the appropriate URL for your application and your Spreedly environment key.

When this form is submitted, it will POST directly to Spreedly’s payment_methods endpoint and it will then be redirected to the endpoint you specified. Much like the iFrame, Spreedly will return a unique payment method token to be used for the next steps:

http://example.com/transparent_redirect_complete?token=XRxaRbWNvufMKqdzOr8cPqs4LWJ

The payment method that is created is effectively just a placeholder needed to complete the remaining transaction:

{
"updated_at": "2020-12-15T16:35:57Z",
"token": "GLadV4SuzrVBAuPAem2se5Pxawq",
"test": false,
"storage_state": "cached",
"payment_method_type": "sprel",
"metadata": null,
"errors": [],
"email": null,
"data": null,
"created_at": "2020-12-15T16:35:57Z",
"callback_url": null
}

In a one-time payment scenario such as this, the primary customer data is really stored at PayPal. Spreedly is facilitating the transactions and does not maintain a payment method like that of a credit card with name, credit number, etc. You may optionally add those details if you like but it is not necessary.

Initiating a Purchase

Once you have an offsite payment method in hand, the next step is to run a purchase:

curl https://core.spreedly.com/v1/gateways/LlkjmEk0xNkcWrNixXa1fvNoTP4/purchase.xml \
  -u 'Ll6fAtoVSTyVMlJEmtpoJV8S:RKOCG5D8D3fZxDSg504D0IxU2XD4Io5VXmyzdCtTivHFTTSy' \
  -H 'Content-Type: application/xml' \
  -d '<transaction>
    <amount>100</amount>
    <currency_code>USD</currency_code>
    <payment_method_token>QReVWckhVm0ZME2IyKMKHoDPG44</payment_method_token>
    <redirect_url>https://example.com/handle_redirect</redirect_url>
    <callback_url>https://example.com/handle_callback</callback_url>
  </transaction>'

Note the required redirect_url and callback_url fields, these should point to listening endpoints on your servers. They will be ignored if you’re running a credit card transaction, but in the case of an offsite transaction they will be used to give you results after the customer completes their transaction. More on those below.

<transaction>
  <on_test_gateway type="boolean">false</on_test_gateway>
  <created_at type="dateTime">2015-01-08T20:57:50Z</created_at>
  <updated_at type="dateTime">2015-01-08T20:57:50Z</updated_at>
  <succeeded type="boolean">false</succeeded>
  <state>pending</state>
  <token>Y3XPb5f1TFNuy8r3AtGI8UB11PQ</token>
  <transaction_type>OffsitePurchase</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>
  <reference nil="true"/>
  <message key="messages.transaction_pending">Pending</message>
  <gateway_token>8XJtbE1p4NTZ6fFqwwn0GrkjEmW</gateway_token>
  <api_urls>
    <callback_conversations>http://core.spreedly.dev/v1/callbacks/XjCHPJad2pDbIKKq2fOJezXmxiM/conversations.xml</callback_conversations>
  </api_urls>
  <payment_method>
    <token>W1lyon1vGzv2MIXAuDmqG7zOwau</token>
    <created_at type="dateTime">2015-01-08T15:57:50-05:00</created_at>
    <updated_at type="dateTime">2015-01-08T15:57:50-05:00</updated_at>
    <email>[email protected]</email>
    <data nil="true"/>
    <storage_state>cached</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/Y3XPb5f1TFNuy8r3AtGI8UB11PQ</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/Y3XPb5f1TFNuy8r3AtGI8UB11PQ/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/Y3XPb5f1TFNuy8r3AtGI8UB11PQ</checkout_url>
    <created_at type="dateTime">2015-01-08T20:57:50Z</created_at>
    <updated_at type="dateTime">2015-01-08T20:57:50Z</updated_at>
  </setup_response>
</transaction>

This is where the process diverges from traditional payments. Note the state field of the transaction: it is set to pending. We’ve also received back a status code of 202, which indicates that this transaction isn’t complete - rather it needs further processing in order to actually collect payment.

To do the further processing, you simply need to redirect the customer’s browser to the checkout_url returned in the transaction.

Redirecting to this URL will land the customer on the offsite page they will use to authorize payment. In this case we’re using Spreedly’s built-in fake payment method Sprel, so the browser lands on a page that looks like this:

In the case of a real offsite gateway, this will not be a page on spreedly.com, but rather will be on paypal.com or the correct payment site. With a live PayPal gateway, the checkout_url will look something like:

"checkout_url": "https://www.paypal.com/checkoutnow?token=86828328C31528625"

When the user is redirected to the URL, it will prompt the user to login at PayPal and then present them with page similar to the following to select the payment method they would like complete the transaction:

Once the user has selected a payment method at PayPal, they will be redirected back to the location originally specified as the redirect_url in the purchase() call.

Finalizing the transaction

Once the customer completes the offsite flow, their browser will be sent back to the redirect_url that you specified when creating the transaction:

http://example.com/transparent_redirect_complete?token=XRxaRbWNvufMKqdzOr8cPqs4LWJ\

Note the field appended to the redirect_url. The transaction_token allows you to distinguish which transaction this redirect is in regards to. You can then lookup details on the transaction and see exactly what happened:

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

The succeeded attribute is now true and we consider the transaction complete. From here you would redirect your customer to a “Thank You” page of some kind, and continue processing their order now that it has been paid.

<transaction>
  <on_test_gateway type="boolean">false</on_test_gateway>
  <created_at type="dateTime">2015-01-08T16:03:38-05:00</created_at>
  <updated_at type="dateTime">2015-01-08T16:03:42-05:00</updated_at>
  <succeeded type="boolean">true</succeeded>
  <state>succeeded</state>
  <token>PMI6UMjiYDM3AUWp6nrH2Zjtt60</token>
  <transaction_type>OffsitePurchase</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>
  <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-08T16:03:38-05:00</created_at>
    <updated_at type="dateTime">2015-01-08T16:03:42-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>NUtqul2Zhp8v5eSzS8DvDduV1EK</token>
    <created_at type="dateTime">2015-01-08T16:03:38-05:00</created_at>
    <updated_at type="dateTime">2015-01-08T16:03:38-05:00</updated_at>
    <email>[email protected]</email>
    <data nil="true"/>
    <storage_state>used</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/PMI6UMjiYDM3AUWp6nrH2Zjtt60</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/PMI6UMjiYDM3AUWp6nrH2Zjtt60/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/PMI6UMjiYDM3AUWp6nrH2Zjtt60</checkout_url>
    <created_at type="dateTime">2015-01-08T16:03:38-05:00</created_at>
    <updated_at type="dateTime">2015-01-08T16:03:42-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-08T16:03:38-05:00</created_at>
    <updated_at type="dateTime">2015-01-08T16:03:42-05:00</updated_at>
  </redirect_response>
</transaction>

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). Though you may receive more than one callback per initiated offsite transaction, subsequent callbacks can be safely ignored if the transaction has already been handled.

In some cases, such as a Paypal ACH, 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:55:48-05:00</created_at>
    <updated_at type="dateTime">2015-01-08T15:55:49-05:00</updated_at>
    <succeeded type="boolean">true</succeeded>
    <state>succeeded</state>
    <token>M0n7624DWjWkALwwAqmsS1Ye7zK</token>
    <transaction_type>OffsitePurchase</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>
    <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:55:48-05:00</created_at>
      <updated_at type="dateTime">2015-01-08T15:55:49-05:00</updated_at>
    </response>
    <api_urls>
      <callback_conversations>http://core.spreedly.dev/v1/callbacks/XjCHPJad2pDbIKKq2fOJezXmxiM/conversations.xml</callback_conversations>
    </api_urls>
    <signed>
      <signature>f5b701255eedbe1da6562b535bfbcd3b35fd944c</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>Jv0mXKkKmzVa6f2b7phO8Mxku0N</token>
      <created_at type="dateTime">2015-01-08T15:55:48-05:00</created_at>
      <updated_at type="dateTime">2015-01-08T15:55:48-05:00</updated_at>
      <email>[email protected]</email>
      <data nil="true"/>
      <storage_state>used</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/M0n7624DWjWkALwwAqmsS1Ye7zK</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/M0n7624DWjWkALwwAqmsS1Ye7zK/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/M0n7624DWjWkALwwAqmsS1Ye7zK</checkout_url>
      <created_at type="dateTime">2015-01-08T15:55:48-05:00</created_at>
      <updated_at type="dateTime">2015-01-08T15:55: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 nil="true"/>
      <fraud_review type="boolean">false</fraud_review>
      <created_at type="dateTime">2015-01-08T15:55:48-05:00</created_at>
      <updated_at type="dateTime">2015-01-08T15:55:49-05:00</updated_at>
    </redirect_response>
  </transaction>
  <transaction>
    <on_test_gateway type="boolean">false</on_test_gateway>
    <created_at type="dateTime">2015-01-08T15:56:20-05:00</created_at>
    <updated_at type="dateTime">2015-01-08T15:56:20-05:00</updated_at>
    <succeeded type="boolean">true</succeeded>
    <state>succeeded</state>
    <token>Q8cXyELAy9mzAoV1TGgyArcDW97</token>
    <transaction_type>OffsitePurchase</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>
    <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:56:20-05:00</created_at>
      <updated_at type="dateTime">2015-01-08T15:56:20-05:00</updated_at>
    </response>
    <api_urls>
      <callback_conversations>http://core.spreedly.dev/v1/callbacks/XjCHPJad2pDbIKKq2fOJezXmxiM/conversations.xml</callback_conversations>
    </api_urls>
    <signed>
      <signature>d5e19b07f3e867d4afdc0552c6f2b2d018e2d718</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>RPenzCeiKLKM0cF9lzIVV2shnXG</token>
      <created_at type="dateTime">2015-01-08T15:56:19-05:00</created_at>
      <updated_at type="dateTime">2015-01-08T15:56:20-05:00</updated_at>
      <email>[email protected]</email>
      <data nil="true"/>
      <storage_state>used</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/Q8cXyELAy9mzAoV1TGgyArcDW97</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/Q8cXyELAy9mzAoV1TGgyArcDW97/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/Q8cXyELAy9mzAoV1TGgyArcDW97</checkout_url>
      <created_at type="dateTime">2015-01-08T15:56:20-05:00</created_at>
      <updated_at type="dateTime">2015-01-08T15:56:20-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:56:20-05:00</created_at>
      <updated_at type="dateTime">2015-01-08T15:56:20-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.

Callback Debugging

Spreedly not calling your callback_url? In the transaction XML, you’ll notice an api_urls element that has a conversations url - if you call that url via the API, you’ll be able to see a history of all of our attempts to contact your callback url, similar to the transcript we save of conversations with the gateway. To limit the number of conversations returned by the conversations url, you can use a count parameter with a value between 20 and 100. The order and since_token parameters can also be used as described here.

Note that the callback URL must utilize HTTPS and a default port of 443. Otherwise, the outgoing callback from Spreedly will time out before reaching your callback endpoint.

Testing

When using a test gateway, transactions are not immediately settled. A transaction will move to a state of ‘pending’ to indicate that a redirect to an offsite authorization page is required. That offsite page may cause the transaction to settle by moving to a succeeded, failed, or gateway_processing_failed state. Or, it could end up being a delayed transaction in the processing state to indicate that the transaction has been accepted but funds have not yet been received (like a Paypal ACH). For a gateway like Paypal, you’ll eventually be notified via a callback when the state of the transaction changes.

For the Test Gateway though, we provide an API call you can use to settle all transactions which are currently in the processing state. When the transactions are settled, you’ll then be notified via callback of the transaction changes. This allows you to test the full life cycle of an offsite delayed transaction.

This is what the API call looks like:

curl https://core.spreedly.com/v1/gateways/LlkjmEk0xNkcWrNixXa1fvNoTP4/settle.xml \
  -u 'C7cRfNJGODKh4Iu5Ox3PToKjniY:4UIuWybmdythfNGPqAqyQnYha6s451ri0fYAo4p3drZUi7q2Jf4b7HKg8etDtoKJ' \
  -H 'Content-Type: application/xml' \
  -d '<settlement>
        <state>gateway_processing_failed</state>
      </settlement>'

You’ll receive a 200(OK) HTTP response code on success. You may specify one of the following terminal states: gateway_processing_failed or succeeded.

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.

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

The state element will change to reflect failures when processing:

<transaction>
  <on_test_gateway type="boolean">false</on_test_gateway>
  <created_at type="dateTime">2015-01-08T16:03:56-05:00</created_at>
  <updated_at type="dateTime">2015-01-08T16:04:02-05:00</updated_at>
  <succeeded type="boolean">false</succeeded>
  <state>gateway_processing_failed</state>
  <token>8nPmoMc9HHl024jgZEjeoyE4rVP</token>
  <transaction_type>OffsitePurchase</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>
  <reference>TheReferenceId</reference>
  <message>A failure response was selected.</message>
  <gateway_token>8XJtbE1p4NTZ6fFqwwn0GrkjEmW</gateway_token>
  <response>
    <success type="boolean">false</success>
    <message>A failure response was selected.</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-08T16:03:56-05:00</created_at>
    <updated_at type="dateTime">2015-01-08T16:04:02-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>OxsImbp9Io71ze8p9jcs1VUTpIX</token>
    <created_at type="dateTime">2015-01-08T16:03:56-05:00</created_at>
    <updated_at type="dateTime">2015-01-08T16:03:56-05:00</updated_at>
    <email>[email protected]</email>
    <data nil="true"/>
    <storage_state>cached</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/8nPmoMc9HHl024jgZEjeoyE4rVP</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/8nPmoMc9HHl024jgZEjeoyE4rVP/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/8nPmoMc9HHl024jgZEjeoyE4rVP</checkout_url>
    <created_at type="dateTime">2015-01-08T16:03:56-05:00</created_at>
    <updated_at type="dateTime">2015-01-08T16:04:02-05:00</updated_at>
  </setup_response>
  <redirect_response>
    <success type="boolean">false</success>
    <message>A failure response was selected.</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-08T16:03:56-05:00</created_at>
    <updated_at type="dateTime">2015-01-08T16:04:02-05:00</updated_at>
  </redirect_response>
</transaction>

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.