Creating a PayPal IPN Web API Endpoint

PayPal's IPN service allows you to perform server side business logic once you are sure that payment has been received. This is extremely useful if you want to hold off on shipping order until payment has arrived. However, as convenient as it is, it is equally under-documented, and there are little resources for a .NET developer. Here is one way of setting up a PayPal IPN endpoint using Web API.

    
    [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;
            }
        }
    }


Above is a pretty basic API Controller. It has one method (ReceiveIPN) that will be used by PayPal to notify us when a payment has been completed. The PayPalController uses a PayPalValidator, which we instantiate in the constructor of the controller. A better solution (and the solution I went with) would be to pass in the validator as a dependency, in order to facilitate unit testing, but IoC containers are a bit out of the scope of this post.
Also, note that the endpoint takes an IPNBindingModel as a parameter. This should have the properties that you need to check from PayPal's IPN (a list of all those properties can be found here.) Don't worry about making sure the property names match up with the properties on the list exactly (I prefer Pascal casing my variables) because we're going to create a Custom Model Binder to handle the mapping between PayPal's posted data, and our binding model. You will need a property to hold the raw request though, because the PayPalValidator needs that.

The PayPalValidator is actually pretty important. When your webservice receives a notification stating that a payment has been made, you really don't know whether or not that notification actually came from PayPal. It could be an attempt by a hacker to trick your system! The way PayPal gets around this is by requiring that you check back with them to ensure that they in fact did send that message. That's what the PayPalValidator does.


    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/";

            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;
        }
    }


Basically, this validator handles the validation steps outlined here. You're able to use it as is to ensure that your PayPal IPN is actually valid. Be sure to note that the GetPayPalResponse function takes a boolean value that determines whether or not you want to use the PayPal sandbox for testing. You'll definitely want to change that before deploying.


Finally, you may have noticed that the ValidateIPN function on the Validator needs the actual message body that was sent to your web service. This is a little tricky to do with Web API, since it automatically binds the incoming POST to a binding model (IPNBindingModel). In order to get around this, and get access to the raw post message, I created a custom model binder.


    
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<string, string> ParsePaypalIPN(string postedRaw)
        {
            var result = new Dictionary<string, string>();
            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;
        }
    }
}

This ModelBinder takes in the raw request, parses the key-value pairs, and creates a IPNBindingModel from the request. The nice thing about this approach is that you can add the raw request string as a member of the BindingModel. This makes unit testing the controller much cleaner, and it also lets you define your IPNBindingModel however you'd like. It's a layer of abstraction, since you have no control over the format of the data that PayPal sends you.

All that's left is to configure WebApi to use your ModelBinder. In the WebApiConfig.cs class, just add this to your Register function


        
        public static void Register(HttpConfiguration config)
        {
            config.MapHttpAttributeRoutes();
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            // ADD THIS LINE
            config.BindParameter(typeof(IPNBindingModel), new IPNModelBinder());
        }

And that about does it. Once you've hosted this web service somewhere, you can set the url to this endpoint in PayPal's dashboard, and begin receiving notifications.

Comments

  1. This comment has been removed by the author.

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. How did you used Dictionary without assigning 2 type arguments ?????

    ReplyDelete
    Replies
    1. Hi Mike,

      That was just a typo. I had to change a couple of things before posting.

      Thanks,
      Carlos

      Delete
  4. I have been trying for a couple of days now to get your code to work and although the paypal IPN simulator returns a valid handshake I have not been able to log any of the calls to the listener. Any tips?

    ReplyDelete
  5. Thank you, Carlos! Works perfectly with additions from https://stackoverflow.com/questions/26638857/is-there-any-sample-for-paypal-ipn

    Here is the model code

    public class IPNBindingModel
    {
    public string PaymentStatus { get; set; }
    public string RawRequest { get; set; }
    public string CustomField { get; set; }
    }

    Also model binder can be registered like this

    public void Post([ModelBinder(typeof(IPNModelBinder))]IPNBindingModel model)

    https://docs.microsoft.com/en-us/aspnet/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api

    ReplyDelete
  6. If you are looking for the method how to cash out bitcoins to paypal account in short time then you need to do this simple task and you will receive your money in paypal account. we are currently working on a project regarding bitcoin payment methods. there are few option for you to cash your bitcoins and we are working on payment method to introduce new method however all old methods we are offering are genuine and working. we are providing fast services to our customers do v!sit and conform your exchange.

    ReplyDelete
  7. This comment has been removed by the author.

    ReplyDelete
  8. This comment has been removed by the author.

    ReplyDelete
  9. Thanks for sharing the code details. Here is my blog where I share valuable information too, related to education and technology. Please visit my touch typing blog.

    ReplyDelete

Post a Comment

Popular posts from this blog

Communicating with your iOS app over USB (C# and/or Xamarin)

Strategy Pattern: Switch Statements, Begone!