Paypal Payflow Transparent Redirect, SecureToken with AJAX?
After spending a bunch of time with a Paypal engineer I've successfully figured out a solution for the Paypal's Payflow Transparent Redirect without hosted pages (have own payment page). Again, here's the documentation which, per the engineer, is pretty confusing: Payflow API Documentation. Also, the code isn't optimized as it was just a R&D app, but as a whole, it is working for me. Just an example and explanation, and I'm sure there are better ways of doing individual steps. Hope this helps and allows you to bypass some of the roadblocks that have been slowing down your Paypal Payflow integration.
YES, it is PCI compliant in that no secure customer data will hit your own servers. Remember that PCI compliance is pretty complicated and involved but this is big part of it. Ok, so I'll explain what I did to make this work in a MVC C# environment. I'll explain the steps here, then include code below.
- CLIENT: Client finishes adding items to the cart and presses BUY button. Javascript handles the button click, doesn't submit, and takes you to the next step.
- CLIENT --> SERVER: AJAX function POSTS to server method to contact Paypal for the single-use secure token. This communication identifies YOU (the merchant) to paypal with your authentication, a unique transaction id (a guid), and non secure details about the transaction (total, billing info, shipping info, return URL details). This way, all your merchant personal acct info is secure (web server to Paypal).
- SERVER --> CLIENT: From the transaction above you'll receive a parameter string that contains the secure token (among other stuff, see method with example). Using this piece of info, I dynamically create my url that I'll eventually need on the client for the transparent redirect part, and send the url string back to the client.
- CLIENT: Using the url that was returned in step #3, I complete the URL by adding the needed credit card parameters using jQuery.
- CLIENT --> PAYPAL: This is where I didn't understand what to do. While step #2 was a post, this step will be a REDIRECT. Sure, that seems appropriate seeing it's called 'transparent redirect', but that part just didn't make sense to me. So, once your entire URL is complete, you'll literally redirect the window to Paypal for processing your transaction.
- PAYPAL --> SERVER: PayPal posts back to one of the URLs you included in step 2 (to a public method on one of my controllers), and I read the response object and parse the parameters.
Easy, right? Perhaps, but for me step 5 caused me big problems. I was using a POST and didn't understand why I kept getting errors on the response. It was an html page with something about an invalid merchant or authentication. Remember to redirect, not post for step #5.
CODE:
STEP 1: onclick attribute on button to call GetToken function.
STEP 2 and STEP 3:
client-side:
function GetToken() {
$.ajax({
url: '@Url.Action("GetToken", "MyController")',
type: 'POST',
cache: 'false',
contentType: 'application/json; charset=utf-8',
dataType: 'text',
success: function (data) {
// data is already formatted in parameter string
SendCCDetailsToPaypal(data);
},
//error:
//TODO Handle the BAD stuff
});}
Server Side:
I have separate methods used to build all the parameter values needed for the token request. First three build: authentication, transaction details, transparent redirect. I keep urls and payflow acct info in a web.config file. Last method, ProcessTokenTransaction, does all the heavy lifting to contact Paypal via WebRequest, and then parse it into the URL that will be sent back to the client. This method should be refactored for a cleaner delivery, but I'll leave that up to you. ParseResponse is a method that populates a simple model that I created, and returns that model.
URL for token (sandbox): https://pilot-payflowpro.paypal.com
THIS IS DIFFERENT THAN THE TOKEN URL!! Used in the PaypalTranactionAPI config value.
URL for transaction: (sandbox) https://pilot-payflowlink.paypal.com
private string PrepareApiAuthenticationParams()
{
var paypalUser = ConfigurationManager.AppSettings["PaypalUser"];
var paypalVendor = ConfigurationManager.AppSettings["PaypalVendor"];
var paypalPartner = ConfigurationManager.AppSettings["PaypalPartner"];
var paypalPw = ConfigurationManager.AppSettings["PaypalPwd"];
//var amount = (decimal)19.53;
var apiParams = @"USER=" + paypalUser
+ "&VENDOR=" + paypalVendor
+ "&PARTNER=" + paypalPartner
+ "&PWD=" + paypalPw
+ "&TENDER=C"
+ "&TRXTYPE=A"
+ "&VERBOSITY=HIGH";
// find more appropriate place for this param
//+ "&VERBOSITY=HIGH";
return apiParams;
}
private string PrepareTransactionParams(CustomerDetail detail)
{
var currencyType = "USD";
var transactionParams = @"&BILLTOFIRSTNAME=" + detail.FirstName
+ "&BILLTOLASTNAME=" + detail.LastName
+ "&BILLTOSTREET=" + detail.Address1
+ "&BILLTOSTREET2=" + detail.Address2
+ "&BILLTOCITY=" + detail.City
+ "&BILLTOSTATE=" + detail.State
//+ "&BILLTOCOUNTRY=" + detail.Country + // NEEDS 3 digit country code
+ "&BILLTOZIP=" + detail.Zip
+ "&BILLTOPHONENUM=" + detail.PhoneNum
+ "&EMAIL=" + detail.Email
+ "&CURRENCY=" + currencyType
+ "&AMT=" + GET_VALUE_FROM_DB
+ "&ERRORURL= " + HostUrl + "/Checkout/Error"
+ "&CANCELURL=" + HostUrl + "/Checkout/Cancel"
+ "&RETURNURL=" + HostUrl + "/Checkout/Success";
// ADD SHIPTO info for address validation
return transactionParams;
}
private string PrepareTransparentParams(string requestId, string transType)
{
var transparentParams = @"&TRXTYPE=" + transType +
"&SILENTTRAN=TRUE" +
"&CREATESECURETOKEN=Y" +
"&SECURETOKENID=" + requestId;
return transparentParams;
}
// Method to build parameter string, and create webrequest object
public string ProcessTokenTransaction()
{
var result = "RESULT=0"; // default failure response
var transactionType = "A";
var secureToken = string.Empty;
var requestId = Guid.NewGuid().ToString().Replace("-", string.Empty);
var baseUrl = ConfigurationManager.AppSettings["PaypalGatewayAPI"];
var apiAuthenticationParams = PrepareApiAuthenticationParams();
// Create url parameter name/value parameter string
var apiTransactionParams = PrepareTransactionParams(detail);
// PCI compliance, Create url parameter name/value parameter string specific to TRANSAPARENT PROCESSING
var transparentParams = PrepareTransparentParams(requestId, transactionType);
var url = baseUrl;
var parameters = apiAuthenticationParams + apiTransactionParams + transparentParams;
// base api url + required
var request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "POST";
request.ContentType = "text/name"; // Payflow?
request.Headers.Add("X-VPS-REQUEST-ID", requestId);
byte[] bytes = Encoding.UTF8.GetBytes(parameters);
request.ContentLength = bytes.Length;
Stream requestStream = request.GetRequestStream();
requestStream.Write(bytes, 0, bytes.Length);
requestStream.Close();
WebResponse response = request.GetResponse();
Stream stream = response.GetResponseStream();
StreamReader reader = new StreamReader(stream);
try
{
// sample successful response
// RESULT=0&RESPMSG=Approved&SECURETOKEN=9pOyyUMAwRUWmmv9nMn7zhQ0h&SECURETOKENID=5e3c50a4c3d54ef8b412e358d24c8915
result = reader.ReadToEnd();
var token = ParseResponse(result, requestId, transactionType);
var transactionUrl = ConfigurationManager.AppSettings["PaypalTransactionAPI"];
secureToken = transactionUrl + "?SECURETOKEN=" + token.SecureToken + "&SECURETOKENID=" + requestId;
//ameValueCollection parsedParams = HttpUtility.ParseQueryString(result);
stream.Dispose();
reader.Dispose();
}
catch (WebException ex)
{
System.Diagnostics.Trace.WriteLine(ex.Message);
}
finally { request.Abort(); }
return secureToken;
}
private TokenResponse ParseResponse(string response, string requestId, string transactionType)
{
var nameValues = HttpUtility.ParseQueryString(response);
int result = -999; // invalid result to guarantee failure
int.TryParse(nameValues.Get(TokenResponse.ResponseParameters.RESULT.ToString()), out result);
// retrieving response message
var responseMessage = nameValues.Get(TokenResponse.ResponseParameters.RESPMSG.ToString());
// retrieving token value, if any
var secureToken = nameValues.Get(TokenResponse.ResponseParameters.SECURETOKEN.ToString());
var reference = nameValues.Get(TokenResponse.ResponseParameters.PNREF.ToString());
var authCode = nameValues.Get(TokenResponse.ResponseParameters.AUTHCODE.ToString());
var cscMatch = nameValues.Get(TokenResponse.ResponseParameters.CSCMATCH.ToString());
// populating model with values
var tokenResponse = new TokenResponse
{
Result = result,
ResponseMessage = responseMessage,
SecureToken = secureToken,
TransactionIdentifierToken = requestId,
TransactionType = transactionType,
ReferenceCode = reference,
AuthorizationCode = authCode,
CSCMatch = cscMatch
};
return tokenResponse;
}
STEP 4 and STEP 5:
Back to Client Side:
Here I use the URL built from the previous steps and add the final needed params (secure credit card info) using jQuery and then REDIRECT to Paypal.
function SendCCDetailsToPaypal(secureParm) {
//alert('in SendCCDetailsToPaypal:' + secureParm);
var secureInfo = '&ACCT=' + $('#ccNumber').val() + '&EXPDATE=' + $("#expMonth").val() + $("#expYear").val() + "&CSC=" + $('#ccSecurityCode').val();
secureInfo = secureParm + secureInfo;
window.location.replace(secureInfo);
}
STEP 6:
Paypal will post back to one of the following methods: Cancel, Error, or Return (name the methods anything you want in the token request). Parse the Response and look at the variables returned from Paypal, particularly the RESULT and RESPMSG. Read the documentation for specifics as you can incorporate address validation and a bunch of other features. Based on the response, display what's appropriate.
server side:
public ActionResult Cancel()
{
var result = ParseRequest(HttpUtility.UrlDecode(Request.Params.ToString()));
//return View("Return", result);
}
public ActionResult Error()
{
var result = ParseRequest(HttpUtility.UrlDecode(Request.Params.ToString()));
return View("Return", result);
}
public ActionResult Return()
{
var result = ParseRequest(HttpUtility.UrlDecode(Request.Params.ToString()));
return View("Return", result);
}
Hope this helps, and good luck! I'll answer clarification questions as I'm able. Thanks for checking this out, and remember to pay it forward.
I was able to use RichieMN's answer to get a working Transparent Redirect happening. However, the problem with doing a redirect with window.location.replace in the SendCCDetailsToPaypal function is that you're passing the data on a GET string.
This works on the PayFlow Gateway side, but when they send the customer's browser back to your ResponseURL, your Apache logs will show the whole payflowlink.paypal.com URL, including the GET string as the referrer in your Apache access logs! That GET string includes the Credit Card number and now you have just lost your PCI compliance!
To alleviate this problem, you can either put the SecureToken and SecureTokenID into your Credit Card entry form, and POST it directly to payflowlink.paypal.com, or you can rewrite the SendCCDetailsToPaypal function to build a form and submit it, like this:
function SendCCDetailsToPaypal() {
var parameters = {
"SECURETOKEN": secureToken,
"SECURETOKENID": secureTokenID,
"ACCT": $("#ccNumber").val(),
"EXPDATE": $("#expMonth").val() + $("#expYear").val(),
"CSC": $("#ccSecurityCode").val()
};
var form = $('<form></form>');
form.attr("method", "post");
form.attr("action", "https://pilot-payflowlink.paypal.com");
$.each(parameters, function(key, value) {
var field = $('<input></input>');
field.attr("type", "hidden");
field.attr("name", key);
field.attr("value", value);
form.append(field);
});
$(document.body).append(form);
form.submit();
}
Since that form transfers the data via POST, when your server gets the result POST back, the referrer does not contain any sensitive data, and your PCI compliance is maintained.