Transparent redirect form

There are a variety of ways to send payment data to Spreedly. Prior to 2015, the recommended way was to use a custom payment form with a transparent redirect to navigate back from Spreedly.

While this method is still documented here, PCI DSSv3.0 and later does not consider this method appropriate for minimizing the compliance burden on transactions. Instead, we recommend the iFrame payment form to incur minimal PCI scope.

Build a payment form

In building the form, input field names are critical, but there is complete control over style, layout and UX for the form:

<form accept-charset="UTF-8" action="https://core.spreedly.com/v1/payment_methods" method="POST">
  <fieldset>
    <input name="redirect_url" type="hidden" value="http://example.com/transparent_redirect_complete" />
    <input name="environment_key" type="hidden" value="C7cRfNJGODKh4Iu5Ox3PToKjniY" />

    <label for="credit_card_first_name">First name</label>
    <input id="credit_card_first_name" name="credit_card[first_name]" type="text" />

    <label for="credit_card_last_name">Last name</label>
    <input id="credit_card_last_name" name="credit_card[last_name]" type="text" />

    <label for="credit_card_number">Card Number</label>
    <input autocomplete="off" id="credit_card_number" name="credit_card[number]" type="text" />

    <label for="credit_card_verification_value">Security Code</label>
    <input autocomplete="off" id="credit_card_verification_value" name="credit_card[verification_value]" type="text" />

    <label for="credit_card_month">Expires on</label>
    <input id="credit_card_month" name="credit_card[month]" type="text" />
    <input id="credit_card_year" name="credit_card[year]" type="text" />

    <button type='submit'>Submit Payment</button>
  </fieldset>
</form>

Notice the autocomplete="off" attribute on the card and verification value fields. This lets browsers know not to store these fields locally, which is especially important for customers on public devices.

Notice the action attribute – this form POST's directly to the Spreedly API. Once the API has recorded payment information, it will redirect the browser back to the redirect_url specified, along with a token representing the card entered by the customer. This token should be saved in order to run the payment. In the case above, the customer would be sent to this url:

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

Note: An SSL certificate is still required on the payment form page. This gives customers confidence in the payment process, and prevents malicious actors from tampering with the form before the customer sees it. Most modern browsers will include warnings on non-SSL sites collecting payment information.

Adding email and/or billing address to a payment method

Additional fields can be included in the payment form to store things like email, billing address, or phone number with the payment method. For example:

<form accept-charset="UTF-8" action="https://core.spreedly.com/v1/payment_methods" method="POST">
  <fieldset>
    <input name="redirect_url" type="hidden" value="http://example.com/transparent_redirect_complete" />
    <input name="environment_key" type="hidden" value="C7cRfNJGODKh4Iu5Ox3PToKjniY" />

    <label for="email">Email</label>
    <input id="email" name="email" type="text" />

    <label for="credit_card_first_name">First name</label>
    <input id="credit_card_first_name" name="credit_card[first_name]" type="text" />

    <label for="credit_card_last_name">Last name</label>
    <input id="credit_card_last_name" name="credit_card[last_name]" type="text" />

    <label for="credit_card_number">Card Number</label>
    <input id="credit_card_number" name="credit_card[number]" type="text" />

    <label for="credit_card_verification_value">Security Code</label>
    <input id="credit_card_verification_value" name="credit_card[verification_value]" type="text" />

    <label for="credit_card_company">Company</label>
    <input id="credit_card_company" name="credit_card[company]" type="text" />

    <label for="credit_card_address1">Address</label>
    <input id="credit_card_address1" name="credit_card[address1]" type="text" />

    <label for="credit_card_address2">Address 2</label>
    <input id="credit_card_address2" name="credit_card[address2]" type="text" />

    <label for="credit_card_city">City</label>
    <input id="credit_card_city" name="credit_card[city]" type="text" />

    <label for="credit_card_state">State</label>
    <input id="credit_card_state" name="credit_card[state]" type="text" />

    <label for="credit_card_zip">Zip</label>
    <input id="credit_card_zip" name="credit_card[zip]" type="text" />

    <label for="credit_card_country">Country</label>
    <input id="credit_card_country" name="credit_card[country]" type="text" />

    <label for="credit_card_phone_number">Phone</label>
    <input id="credit_card_phone_number" name="credit_card[phone_number]" type="text" />

    <label for="credit_card_month">Expires on</label>
    <input id="credit_card_month" name="credit_card[month]" type="text" />
    <input id="credit_card_year" name="credit_card[year]" type="text" />

    <button type='submit'>Submit Payment</button>
  </fieldset>
</form>

Which sends the resulting payment method token to the URL at http://example.com/transparent_redirect_complete?token=27f07ceEYMrxRZrH6yhaNtmdMdJ.

Adding shipping address to a payment method

To add shipping address, specify:

<form accept-charset="UTF-8" action="https://core.spreedly.com/v1/payment_methods" method="POST">
  <fieldset>
    <input name="redirect_url" type="hidden" value="http://example.com/transparent_redirect_complete" />
    <input name="environment_key" type="hidden" value="C7cRfNJGODKh4Iu5Ox3PToKjniY" />

    <label for="credit_card_">First name</label>
    <input id="credit_card_first_name" name="credit_card[first_name]" type="text" />

    <label for="credit_card_last_name">Last name</label>
    <input id="credit_card_last_name" name="credit_card[last_name]" type="text" />

    <label for="credit_card_number">Card Number</label>
    <input id="credit_card_number" name="credit_card[number]" type="text" />

    <label for="credit_card_verification_value">Security Code</label>
    <input id="credit_card_verification_value" name="credit_card[verification_value]" type="text" />

    <label for="credit_card_shipping_address1">Address</label>
    <input id="credit_card_shipping_address1" name="credit_card[shipping_address1]" type="text" />

    <label for="credit_card_shipping_address2">Address 2</label>
    <input id="credit_card_shipping_address2" name="credit_card[shipping_address2]" type="text" />

    <label for="credit_card_shipping_city">City</label>
    <input id="credit_card_shipping_city" name="credit_card[shipping_city]" type="text" />

    <label for="credit_card_shipping_state">State</label>
    <input id="credit_card_shipping_state" name="credit_card[shipping_state]" type="text" />

    <label for="credit_card_shipping_zip">Zip</label>
    <input id="credit_card_shipping_zip" name="credit_card[shipping_zip]" type="text" />

    <label for="credit_card_shipping_country">Country</label>
    <input id="credit_card_shipping_country" name="credit_card[shipping_country]" type="text" />

    <label for="credit_card_shipping_phone_number">Phone</label>
    <input id="credit_card_shipping_phone_number" name="credit_card[shipping_phone_number]" type="text" />

    <label for="credit_card_month">Expires on</label>
    <input id="credit_card_month" name="credit_card[month]" type="text" />
    <input id="credit_card_year" name="credit_card[year]" type="text" />

    <button type='submit'>Submit Payment</button>
  </fieldset>
</form>

With the resulting payment method token sent to the URL at http://example.com/transparent_redirect_complete?token=EOTYqdRsTRLziEnONa8OQ0FX956.

Specifying full name

To include just a single name field instead of two separate fields for first_name and last_name, use the full_name attribute like so:

<form accept-charset="UTF-8" action="https://core.spreedly.com/v1/payment_methods" method="POST">
  <fieldset>
    <input name="redirect_url" type="hidden" value="http://example.com/transparent_redirect_complete" />
    <input name="environment_key" type="hidden" value="C7cRfNJGODKh4Iu5Ox3PToKjniY" />

    <label for="credit_card_full_name">Name</label>
    <input id="credit_card_full_name" name="credit_card[full_name]" type="text" />

    <label for="credit_card_number">Card Number</label>
    <input id="credit_card_number" name="credit_card[number]" type="text" />

    <label for="credit_card_verification_value">Security Code</label>
    <input id="credit_card_verification_value" name="credit_card[verification_value]" type="text" />

    <label for="credit_card_month">Expires on</label>
    <input id="credit_card_month" name="credit_card[month]" type="text" />
    <input id="credit_card_year" name="credit_card[year]" type="text" />

    <button type='submit'>Submit Payment</button>
  </fieldset>
</form>

Sending the payment method token to the URL at http://example.com/transparent_redirect_complete?token=PyZ2iXiVk7NKTsrHvWuuDQINv7Z.

Tracking customers

After Spreedly's API uses transparent redirect to receive the data from end-customers, there are two ways to then identify those users when they return to the site: using the redirect_url and/or passing custom data through.

Using the redirect_url

In the transparent redirect form, this field can be controlled by the site that renders the form. This means an identifier for customers, their orders, or another identifier can be dropped into the url, for example:

http://example.com/store/order/1234/finish where "1234" is the order number to be completed. This would redirect back to http://example.com/store/order/1234/finish?token=coretoken.

Finish processing the order using the payment method token passed back from the Spreedly API. The API can also handle query parameters in the URL, for example:

http://example.com/store/order?id=1234 would redirect back to http://example.com/store/order?id=1234&token=coretoken.

Using passthrough data

To pass more information than the redirect_url will comfortably contain, or to pass it in a format not-easily accommodated by a URL, there is an ability to pass custom data to the Spreedly API POST and pull it using the returned token data.

To pass data through, specify the data fields in the form:

<form accept-charset="UTF-8" action="https://core.spreedly.com/v1/payment_methods" method="POST">
  <fieldset>
    <input name="redirect_url" type="hidden" value="http://example.com/transparent_redirect_complete" />
    <input name="environment_key" type="hidden" value="C7cRfNJGODKh4Iu5Ox3PToKjniY" />

    <input name="data[how_many]" type="text" value="5" />
    <input name="data[some_other_data]" type="text" value="Here is some additional data" />

    <label for="credit_card_first_name">First name</label>
    <input id="credit_card_first_name" name="credit_card[first_name]" type="text" />

    <label for="credit_card_last_name">Last name</label>
    <input id="credit_card_last_name" name="credit_card[last_name]" type="text" />

    <label for="credit_card_number">Card Number</label>
    <input id="credit_card_number" name="credit_card[number]" type="text" />

    <label for="credit_card_verification_value">Security Code</label>
    <input id="credit_card_verification_value" name="credit_card[verification_value]" type="text" />

    <label for="credit_card_month">Expires on</label>
    <input id="credit_card_month" name="credit_card[month]" type="text" />
    <input id="credit_card_year" name="credit_card[year]" type="text" />

    <button type='submit'>Submit Payment</button>
  </fieldset>
</form>

With the redirect http://example.com/transparent_redirect_complete?token=UdIC5FhelrzkDgwquhpr1Dy2fNz.

The name attribute for the data fields is important. In the form above, there is data[how_many] and data[some_other_data]. Looking at the payment method later will show the data field holding whatever data was passed at the beginning:

curl https://core.spreedly.com/v1/payment_methods/UdIC5FhelrzkDgwquhpr1Dy2fNz.xml \
  -u 'C7cRfNJGODKh4Iu5Ox3PToKjniY:4UIuWybmdythfNGPqAqyQnYha6s451ri0fYAo4p3drZUi7q2Jf4b7HKg8etDtoKJ' \
  -H 'Content-Type: application/xml'
<payment_method>
  <token>UdOMp4OAIbNdcRBjTIVWRYvxhT7</token>
  <created_at type="dateTime">2017-07-27T17:51:40Z</created_at>
  <updated_at type="dateTime">2017-07-27T17:51:40Z</updated_at>
  <email nil="true"/>
  <data>
    <how_many>5</how_many>
    <some_other_data>Here is some additional data</some_other_data>
  </data>
  <storage_state>cached</storage_state>
  <test type="boolean">true</test>
  <last_four_digits>1111</last_four_digits>
  <first_six_digits>411111</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"/>
  <company 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>XXX</verification_value>
  <number>XXXX-XXXX-XXXX-1111</number>
  <fingerprint>e3cef43464fc832f6e04f187df25af497994</fingerprint>
</payment_method>

Names for data fields can be anything, so long as it follows the format of data[name_field_here].

In these data fields, use XML or JSON to pass a data structure through, if desired. This must live within the HTML form and thus could require something like Base64 to encode it if sufficiently complex. In general, there is likely not a requirement to pass much data with transparent redirect. Rather, this data field is intended more as a way to associate some application or specific order information while keeping things on one payment form for the end-customer.

🚧

Note: the data in this field can be modified.

Both data fields and the redirect_url can be tampered-with. This means one should not pass any data through that either the customer should not know about, or that the customer should not be able to edit. For example, item price should not be included here as it would allow the customer to grab the form and change the price of their order, achieving a great discount.

The easiest way to avoid this issue is to avoid sending data that would be valuable to tamper with. Instead of passing total amount, pass through the ID for an invoice in another system, and use that to look up the invoice and pull total amount there. If there is an absolute requirement to include tamper-sensitive data in the passthrough, use a standard like HMAC to tamper-proof it. This is beyond the scope of Spreedly's documentation or recommendations.

Validating payment methods

Customers can make mistakes entering their card number and CVV. It is important to reuse sensitive data when validation fails, and to validate aspects of payment information that the Spreedly API does not require, such as billing address.

Reprocessing sensitive data

In an example where the customer forgets to enter their name, causing the gateway to reject the transaction, we can see:

curl https://core.spreedly.com/v1/gateways/LlkjmEk0xNkcWrNixXa1fvNoTP4/purchase.xml \
  -u 'C7cRfNJGODKh4Iu5Ox3PToKjniY:4UIuWybmdythfNGPqAqyQnYha6s451ri0fYAo4p3drZUi7q2Jf4b7HKg8etDtoKJ' \
  -H 'Content-Type: application/xml' \
  -d '<transaction>
        <payment_method_token>770ZrBI3wS8O9opl9xTJinupaCb</payment_method_token>
        <amount>100</amount>
        <currency_code>USD</currency_code>
    </transaction>'
<transaction>
  <on_test_gateway type="boolean">false</on_test_gateway>
  <created_at type="dateTime">2017-07-27T17:51:57Z</created_at>
  <updated_at type="dateTime">2017-07-27T17:51:57Z</updated_at>
  <succeeded type="boolean">false</succeeded>
  <state>failed</state>
  <token>1gvW5Ufq1uI2uMQOsRJI277ZSSt</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>
  </gateway_specific_response_fields>
  <gateway_transaction_id nil="true"/>
  <gateway_latency_ms 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.payment_method_invalid">The payment method is invalid.</message>
  <gateway_token>T11bJAANtTWnxl36GYjKWvbNK0g</gateway_token>
  <shipping_address>
    <name></name>
    <address1 nil="true"/>
    <address2 nil="true"/>
    <city nil="true"/>
    <state nil="true"/>
    <zip nil="true"/>
    <country nil="true"/>
    <phone_number nil="true"/>
  </shipping_address>
  <api_urls>
  </api_urls>
  <payment_method>
    <token>VBFEdnKRA7Ay8uOl7Afql9hr6Z4</token>
    <created_at type="dateTime">2017-07-27T17:51:56Z</created_at>
    <updated_at type="dateTime">2017-07-27T17:51:56Z</updated_at>
    <email nil="true"/>
    <data nil="true"/>
    <storage_state>cached</storage_state>
    <test type="boolean">true</test>
    <last_four_digits>1111</last_four_digits>
    <first_six_digits>411111</first_six_digits>
    <card_type>visa</card_type>
    <first_name></first_name>
    <last_name></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"/>
    <company nil="true"/>
    <full_name></full_name>
    <eligible_for_card_updater nil="true"/>
    <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>
      <error attribute="first_name" key="errors.blank">First name can't be blank</error>
      <error attribute="last_name" key="errors.blank">Last name can't be blank</error>
    </errors>
    <verification_value>XXX</verification_value>
    <number>XXXX-XXXX-XXXX-1111</number>
    <fingerprint>e3cef43464fc832f6e04f187df25af497994</fingerprint>
  </payment_method>
</transaction>

At this stage, redisplay the form and allow the customer to try again, without requiring them to reenter the correct card number. In the response, all fields are included but the number is shown as XXXX-XXXX-XXXX-1111 to protect all but the last four digits which Spreedly can return.

Passing back the payment_method_token field with the token of a failed payment method will replace sensitive data from the old card with data from the first submission. This allows the reuse of the data from the incorrect submission, without knowing what data is (in)correct.

<form accept-charset="UTF-8" action="https://core.spreedly.com/v1/payment_methods" method="POST">
  <fieldset>
    <input name="redirect_url" type="hidden" value="http://example.com/transparent_redirect_complete" />
    <input name="environment_key" type="hidden" value="C7cRfNJGODKh4Iu5Ox3PToKjniY" />

    <input name="payment_method_token" type="hidden" value="770ZrBI3wS8O9opl9xTJinupaCb" />

    <label for="credit_card_first_name">First name</label>
    <input id="credit_card_first_name" name="credit_card[first_name]" type="text" />

    <label for="credit_card_last_name">Last name</label>
    <input id="credit_card_last_name" name="credit_card[last_name]" type="text" />

    <label for="credit_card_number">Card Number</label>
    <input id="credit_card_number" name="credit_card[number]" value="XXXX-XXXX-XXXX-1111" type="text" />

    <label for="credit_card_verification_value">Security Code</label>
    <input id="credit_card_verification_value" name="credit_card[verification_value]" value="XXX" type="text" />

    <label for="credit_card_month">Expires on</label>
    <input id="credit_card_month" name="credit_card[month]" type="text" />
    <input id="credit_card_year" name="credit_card[year]" type="text" />

    <button type='submit'>Submit Payment</button>
  </fieldset>
</form>

If values for any sensitive fields are at all changes from what was passed back, the new values will take precedence. This means card number or verification values from the old card must be specified if they are to be used over the new card values. If attempts fail, continue to chain them together and Spreedly's API will carry values forward.

Validating extra data

Spreedly's API only does basic validation on the data it gets passed, mostly to ensure there is enough data in the correct format to make a submission to the specified gateway. That said, there is a plethora of optional data that can be passed to the Spreedly API. To check whether any of this extra data is present or that it is of a particular format, request the payment method ahead of running a purchase:

curl https://core.spreedly.com/v1/payment_methods/Rx4fQHJq6ROsz9Qeyg1NJoiPJGn.xml \
  -u 'C7cRfNJGODKh4Iu5Ox3PToKjniY:4UIuWybmdythfNGPqAqyQnYha6s451ri0fYAo4p3drZUi7q2Jf4b7HKg8etDtoKJ'
<payment_method>
  <token>SGzRGIlNHchPA2Y9LzxVhO8KH1G</token>
  <created_at type="dateTime">2017-07-27T17:52:17Z</created_at>
  <updated_at type="dateTime">2017-07-27T17:52:17Z</updated_at>
  <email nil="true"/>
  <data nil="true"/>
  <storage_state>cached</storage_state>
  <test type="boolean">true</test>
  <last_four_digits>1111</last_four_digits>
  <first_six_digits>411111</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"/>
  <company 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>XXX</verification_value>
  <number>XXXX-XXXX-XXXX-1111</number>
  <fingerprint>e3cef43464fc832f6e04f187df25af497994</fingerprint>
</payment_method>

After finding all the required information, simply proceed running the purchase.

Retain

Payment methods are placed in a cached storage stage when first added to Spreedly. Unless told otherwise, the card will be purged from Spreedly's vault after a short period of time (~12 hours). For workflows where payment information is collected and the card is immediately charged, there is nothing to note. However, to charge the card again in the future, or to have a longer period of time between collecting the payment information and executing a purchase, a card must be retained.

Store the payment method token

To do this, create a server action at the redirect_url location that receives redirect request from Spreedly and stores the token parameter, tied to the user's account. In this scenario, the application has full responsibility for the relationship between a user and any stored payment data in Spreedly (via the token reference).