I have an Asp.Net WEB API 2 project and I would like to implement an Instant Payment Notification (IPN) listener controller.
I can\'t find any example and nuget package
Extending Michal Hosala's answer, there are two things needed to get a successful handshake with PayPal
First, setting the security protocol before making request to PayPal
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
Second, avoiding the dictionary because for verification, PayPal requires the data to be posted back in the same order and preceded by the cmd variable. I ended up doing this
Request.InputStream.Seek(0, SeekOrigin.Begin);
string rawRequestBody = new StreamReader(Request.InputStream).ReadToEnd();
var ipnVarsWithCmd = rawRequestBody.Split('&').Select(x => new KeyValuePair<string, string>(x.Split('=')[0], x.Split('=')[1])).ToList();
ipnVarsWithCmd.Insert(0, new KeyValuePair<string, string>("cmd", "_notify-validate"));
This is my code
Feel free to review is something is wrong
[Route("IPN")]
[HttpPost]
public IHttpActionResult IPN()
{
// if you want to use the PayPal sandbox change this from false to true
string response = GetPayPalResponse(true);
if (response == "VERIFIED")
{
//Database stuff
}
else
{
return BadRequest();
}
return Ok();
}
string GetPayPalResponse(bool useSandbox)
{
string responseState = "INVALID";
// Parse the variables
// Choose whether to use sandbox or live environment
string paypalUrl = useSandbox ? "https://www.sandbox.paypal.com/"
: "https://www.paypal.com/";
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(paypalUrl);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));
//STEP 2 in the paypal protocol
//Send HTTP CODE 200
HttpResponseMessage response = client.PostAsJsonAsync("cgi-bin/webscr", "").Result;
if (response.IsSuccessStatusCode)
{
//STEP 3
//Send the paypal request back with _notify-validate
string rawRequest = response.Content.ReadAsStringAsync().Result;
rawRequest += "&cmd=_notify-validate";
HttpContent content = new StringContent(rawRequest);
response = client.PostAsync("cgi-bin/webscr", content).Result;
if(response.IsSuccessStatusCode)
{
responseState = response.Content.ReadAsStringAsync().Result;
}
}
}
return responseState;
}
There is an official c# example here:
https://github.com/paypal/ipn-code-samples
in path \c#\paypal_ipn_mvc.cs
The C# example shows an ASP.NET MVC controller with an action that responds to the IPN.
Based on accepted answer I came up with the following code implementing IPN listener for ASP.NET MVC. The solution has already been deployed and appears to work correctly.
[HttpPost]
public async Task<ActionResult> Ipn()
{
var ipn = Request.Form.AllKeys.ToDictionary(k => k, k => Request[k]);
ipn.Add("cmd", "_notify-validate");
var isIpnValid = await ValidateIpnAsync(ipn);
if (isIpnValid)
{
// process the IPN
}
return new EmptyResult();
}
private static async Task<bool> ValidateIpnAsync(IEnumerable<KeyValuePair<string, string>> ipn)
{
using (var client = new HttpClient())
{
const string PayPalUrl = "https://www.paypal.com/cgi-bin/webscr";
// This is necessary in order for PayPal to not resend the IPN.
await client.PostAsync(PayPalUrl, new StringContent(string.Empty));
var response = await client.PostAsync(PayPalUrl, new FormUrlEncodedContent(ipn));
var responseString = await response.Content.ReadAsStringAsync();
return (responseString == "VERIFIED");
}
}
EDIT:
Let me share my experience - the above code was working just fine up until now, but suddenly it failed for one IPN it was processing, i.e. responseString == "INVALID"
.
The issue turned out to be that my account was set up to use charset == windows-1252
which is PayPal default. However, FormUrlEncodedContent
uses UTF-8 for encoding and therefore the validation failed because of national characters like "ř". The solution was to set charset
to UTF-8, which can be done in Profile > My selling tools > PayPal button language encoding > More Options, see this SO thread.
I was also looking for a solution similar to the OP's original question Is there any IPN listener controller example? At least a PaypalIPNBindingModel to bind the Paypal query.
and I got to this page. I tried the other solutions mentioned in this thread, they all worked but I really need the PayPal query-to-model solution so I googling until I stumbled on Carlos Rodriguez's Creating a PayPal IPN Web API Endpoint blogpost.
Here's an overview on what Carlos did:
Create a model. Base the properties you'll define in the model from the ipn response you'll get from PayPal.
public class IPNBindingModel
{
public string PaymentStatus { get; set; }
public string RawRequest { get; set; }
public string CustomField { get; set; }
}
Create a PayPal Validator class.
public class PayPalValidator
{
public bool ValidateIPN(string body)
{
var paypalResponse = GetPayPalResponse(true, body);
return paypalResponse.Equals("VERIFIED");
}
private string GetPayPalResponse(bool useSandbox, string rawRequest)
{
string responseState = "INVALID";
string paypalUrl = useSandbox ? "https://www.sandbox.paypal.com/"
: "https://www.paypal.com/";
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(paypalUrl);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));
HttpResponseMessage response = client.PostAsJsonAsync("cgi-bin/webscr", "").Result;
if (response.IsSuccessStatusCode)
{
rawRequest += "&cmd=_notify-validate";
HttpContent content = new StringContent(rawRequest);
response = client.PostAsync("cgi-bin/webscr", content).Result;
if (response.IsSuccessStatusCode)
{
responseState = response.Content.ReadAsStringAsync().Result;
}
}
}
return responseState;
}
}
Create your controller.
[RoutePrefix("paypal")]
public class PayPalController : ApiController
{
private PayPalValidator _validator;
public PayPalController()
{
this._validator = new PayPalValidator();
}
[HttpPost]
[Route("ipn")]
public void ReceiveIPN(IPNBindingModel model)
{
if (!_validator.ValidateIPN(model.RawRequest))
throw new Exception("Error validating payment");
switch (model.PaymentStatus)
{
case "Completed":
//Business Logic
break;
}
}
}
Create a model binder that will define how Web Api will automatically create the model for you.
public class IPNModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(IPNBindingModel))
{
return false;
}
var postedRaw = actionContext.Request.Content.ReadAsStringAsync().Result;
Dictionary postedData = ParsePaypalIPN(postedRaw);
IPNBindingModel ipn = new IPNBindingModel
{
PaymentStatus = postedData["payment_status"],
RawRequest = postedRaw,
CustomField = postedData["custom"]
};
bindingContext.Model = ipn;
return true;
}
private Dictionary ParsePaypalIPN(string postedRaw)
{
var result = new Dictionary();
var keyValuePairs = postedRaw.Split('&');
foreach (var kvp in keyValuePairs)
{
var keyvalue = kvp.Split('=');
var key = keyvalue[0];
var value = keyvalue[1];
result.Add(key, value);
}
return result;
}
}
}
Register your model binder to WebApiConfig.cs.
config.BindParameter(typeof(IPNBindingModel), new IPNModelBinder());
Hope this helps somebody else. Thank you Carlos Rodriguez for your amazing code.