Using the PayPal Payment System in ASP.NET

Introduction

Those who create commercial sites are faced with the question: "How do I receive payments?" One of the most popular payment systems in the world is PayPal. This system is often chosen because it is reliable, easy to use, and allows an account to be opened easily. To open an account, you simply need to have a credit card and/or an account in an American bank. One of the shortcomings of the system is its severe security policy. But, practice evidences that if you follow the rules of the system use carefully, errors are very infrequent. The purpose of this article is to show how payments processing can be organized to support reliability and security. The article is also aimed at providing you with an example of developing a simplified version of an online shop to demonstrate interaction with the PayPal system. You can use the code in your applications to organize interaction with the PayPal system and to process payments.

Figure 1: The View Basket page.

The article pays special attention to the process of automatic payment verification using IPN (Instant Payment Notification). The article is based on the experience of KB_Soft Group and the official PayPal documentation.

Types of PayPal Payments

PayPal supports several types of payments:

  • Payments for goods in the PayPal cart. PayPal is responsible for all operations supporting the cart in this case. Unfortunately, this option does not provide the maximum flexibility that is required to implement some projects, so the article does not consider this option.
  • "One click" shopping. Goods are not put into the cart in this case. This method also is used to pay for goods in a cart that was filled without PayPal. That's why this option provides maximum flexibility and full control of the cart.
  • Recurring billing or subscription. PayPal provides a subscription capability; that means that a definite sum will be periodically transferred from the user's account to the seller's account. The user can unsubscribe anytime. The seller can specify the subscription's period and cost. He also can organize a trial period to let the user assess the quality of services he provides. The trial period can be either paid or free.

For the reasons described above, the article will consider the second option. The subscription will not be described to make the example simple. To interact with the PayPal system, KB_Soft Group uses a UserControl that is a product internally developed for these purposes. The given example of the code that works with PayPal uses a special HTML form for the request to make clear the explanation of interaction with the system. PayPal also provides its own control as a dynamic library, but, unfortunately, this control functions correctly only with American locales (en-us). Besides, it does not provide full functionality to work with PayPal. Neither does it provide the flexibility required to work on some projects.

The Payment Process

The payment process is very simple. A POST form is created with a set of hidden fields that contain information about an item (identifier, name, and cost) and a button to send the form. It should be noted that all prices should be expressed with two digits after the decimal point. If an item costs $10, its price should be expressed as "10.00". When the form is sent, the buyer goes to the paypal.com site and finishes the payment process. When a real PayPal account is used, the form should be sent to https://www.paypal.com/cgi-bin/webscr. The developed example also allows you to work with the PayPal Sandbox. Use the "UseSandboxbo" parameter of web.config and the form will be sent to https://www.sandbox.paypal.com/cgi-bin/webscr.

"One-click" shopping

A code of the simplest form:

<form method="post" action= "https://www.paypal.com/cgi-bin/webscr">
   <input type="hidden" name="cmd" value="_xclick">
   <input type="hidden" name="business" value="my@email.com">
   <input type="hidden" name="item_name" value="Item name">
   <input type="hidden" name="item_number" value="1234">
   <input type="hidden" name="amount" value="19.95">
   <input type="hidden" name="no_shipping" value="1">
   <input type="submit" value="Buy Now">
</form>

Description of main parameters

Parameter Description
cmd The parameter is obligatory. It must have the "_xclick" value for an unencrypted request.
business The parameter is obligatory. It represents the seller's e-mail.
item_number This parameter is an item identifier. This value will not be shown to the user; however, it will be passed to your script at the time of transaction confirmation. If you plan to use PayPal to pay for goods in a cart, you can pass the cart's identifier in this parameter.
item_name This is a name of the item that will be shown to the user.
no_shipping This parameter determines whether the delivery address should be requested. "1" means that the address will be requested; "0" means that it will be not.
return This is the URL where the user will be redirected after the payment is successfully performed. If this parameter is not passed, the buyer remains on the PayPal site.
rm This parameter determines the way information about a successful transaction will be passed to the script that is specified in the return parameter. "1" means that no parameters will be passed. "2" means that the POST method will be used. "0" means that the GET method will be used. The parameter is "0" by default.
cancel_return This is the URL where the user will be redirected when he cancels the payment. If the parameter is not passed, the buyer remains on the PayPal site.
notify_url This is the URL where PayPal will pass information about the transaction (IPN). If the parameter is not passed, the value from the account settings will be used. If this value is not defined in the account settings, IPN will not be used.
custom This field does not take part in the shopping process. It simply will be passed to the IPN script at the time of transaction confirmation.
invoice This parameter is used to pass the invoice number. The parameter is not obligatory, but being passed, it must be unique for every transaction.
amount This parameter represents an amount of payment. If the parameter is not passed, the user will be allowed to enter the amount (this is used for donations).
currency_code This parameter represents a currency code. Possible values are "USD", "EUR", "GBP", "YEN", "CAD", and so forth. It is "USD" by default.

Table 1: Main parameters of the request form.

Table 1 lists the most often used parameters. See the PayPal documentation for the full list of parameters (see the references at the end of the article).

IPN

IPN (Instant Payment Notification) is a PayPal technology allowing the automation of payments processing. The essence of the technology lies in a special script created on the seller's server. When an event happens related to the seller's account (for example, payment transfer, payment cancel, subscription creation or cancel, and so on), the PayPal server sends a POST request with transaction information to the IPN script. The script in turn sends a request to the PayPal server to verify the transaction.

So, the buyer has performed a payment. After a delay (up to several seconds), the PayPal server sends the request to the IPN script that is specified in the account settings or passed in the notify_url parameter. A good IPN script is a key to payments security. If you have ever heard that sellers who use PayPal are victims of somebody's cheating, be sure that those sellers either do not use IPN at all or have a poor IPN script.

First of all, the script must make sure that it was called by the PayPal server. For these purposes, the script generates a POST request to https://www.paypal.com/cgi-bin/webscr (or to https://www.sandbox.paypal.com/cgi-bin/webscr) and passes all variables it received without any changes together with the cmd parameter having the _notify-validate value. As a response to the request, the script receives either VERIFIED (the transaction was successfully verified) or INVALID (in case of an error). If the script receives INVALID, it must terminate.

Then, the script must check the payment recipient because a potential intruder may change the form for the payment to be sent to his account. The payment recipient is determined by the business and receiver_email variables. Two variables are necessary because PayPal allows several e-mails to be registered for one account. The e-mail that is specified during account creation is the primary one. The receiver_email is always the primary e-mail. If a payment was sent to an additional e-mail, the e-mail is passed in the business parameter. If business and/or receiver_email do not contain an expected value, the script immediately terminates.

Then, the script must check the amount and the currency of the payment. This verification is required because the potential intruder may change the payment amount in the form. In case a subscription is used, the script should check all subscription parameters (what parameters are set, duration and cost of trial periods, duration and cost of the main subscription cycle, and the like).

An IPN for the same transaction can be sent more than once. For example, if a payment was delayed for some reason, the first IPN will be sent immediately after the payment. After the payment is performed or cancelled, the second IPN is sent. If the IPN script does not return the HTTP status equal to 200, PayPal sends the IPN again after some time. The first time it will be repeated in 10 seconds, then in 20 seconds if needed, then in 40, 80, and so forth (up to 24 hours). If script does not respond in four days, PayPal stops sending the IPN. This can be used to not lose transaction information if an error occurs in the IPN script. For example, if the script fails to connect to the database where it stores transaction information, the script can return the HTTP status equal to 500 and the IPN will be repeated later. The repeated IPN will be sent the same way if the IPN script does not refer to the PayPal server to verify the transaction.

As you can see from the description of the return, rm, and notify_url parameters, the IPN can be passed to two scripts specified in the return and notify_url parameters. There are two differences between them:

  • The IPN for return will be sent only once, after the payment is performed. notify_url can be called several times (see the paragraph above).
  • The result of the return script will be shown to the user. It should be noted that if the result contains links, the links must be absolute. The result of the notify_url script is not displayed in the browser.

The received POST variables contain transaction information. The most widely used variables are the following:

Parameter Description
txn_id Unique transaction number
payment_date Payment date in the "18:30:30 Jan 1, 2000 PST" format
payer_email buyer's e-mail
business seller's e-mail
payer_id Unique identifier of the buyer. Those who take part in payments performed with the help of PayPal are identified by an e-mail address, but taking into consideration the possibility to change the e-mail, payer_id should be used for the buyer's identification.
item_number Item identifier
item_name Item name
txn_type Transaction type. Possible values are:
"web_accept": The payment was performed by clicking the "Buy Now" button.
"cart": The payment was performed by using the built-in PayPal cart.
"send_money": The payment was performed by using the "Send money" function.
"reversal": Money was returned to the buyer on his initiative.
payment_status Payment state. Possible values are:
"Completed": Transaction was successfully performed, and money is transferred to the seller's account. If txn_type="reversal", the money is returned to the buyer's account.
"Pending": Payment was delayed. The delay reason is determined in the pending_reason variable. After the payment is complete, PayPal will send another one notification.
"Failed": Payment failed. This state is possible only when the payment was performed from a bank account.
"Denied": Seller cancelled the payment. The payment is in this state when the seller cancels the payment after having had the Pending state before.
"Refunded": Money is returned to buyer. The payment is in this state when seller cancels the payment having the Completed state.
pending_reason Reason of payment delay. Possible values are:
"echeck": Payment was performed with an e-check
"multi_currency": Payment was performed in the currency that is specified in the settings of the seller's account. The payment will be completed when the seller confirms the transaction.
"intl": Seller is not a USA dweller. The payment will be completed when the seller confirms the transaction.
"verify": Seller's account is in the "unverified" state. The payment will be completed when the seller is identified.
"address": Settings of the seller's account require that the buyer should specify the delivery address, but the buyer does not specify the address. The payment will be completed after the seller confirms the transaction.
"upgrade": Payment was performed using a credit card and the seller's account has the "Personal" status. To complete the payment, the seller should upgrade the account up to "Business" or "Premier."
"unilateral": Seller's e-mail is not registered in the system.
"other": Another reason. The seller needs to contact Support to know more about the reason.
payment_type Payment type. Possible values are:
"echeck": Payment was performed with an e-check.
"instant": Payment was performed with a credit card or using a bank account or money from buyer's PayPal account.
mc_gross Payment amount.
mc_fee Commissions charges. The amount that is put on seller's account is determined as mc_gross - mc_fee
mc_currency Payment currency.
first_name Buyer's first name.
last_name Buyer's last name.
address_street Street.
address_city City
address_state State/Region.
address_zip Zip Code.
address_country Country.
verify_sign Digital signature. It is used in PayPal for transaction verification.

Table 2: The most widely used variables.

Using the PayPal Payment System in ASP.NET

An Example of IPN Processing

Following is an example of a script that uses the PayPal IPN. I publish this script not to provide you with a ready script that you can copy/paste, but to illustrate the general principles of working with IPN. KB_Soft Group uses much more complicated scripts to create sites using the PayPal system. This script is rather easy, but at the same time it illustrates the main principles of IPN processing.

The given code creates a simplified version of an online shop. The buyer adds goods into a cart and pays for them. After the buyer pays, a payment report is created.

All information about goods and cart contents is stored in XML files. I chose this way of information storage only for reasons of compatibility and so that any user could download the code and easily adjust and test the created online shop. For real applications, databases should be used to store information about goods, carts, payment requests, and responses to them.

To store information about goods, use the Goods.xml file. It should have the following structure:

<Goods>
   <Good id="0" name="Sample of good" price="10.99" />
</Goods>

where

  • id is a unique item identifier
  • name is the item name
  • price is the item price

To make this example simple, I did not provide the created online shop with the functionality of allowing new items to be added to the merchandise catalogue. But, information about new items can be added manually into the XML file if needed.

To store information about carts, I use the Carts.xml file that has the following structure:

<Carts>
   <Cart rec_id="0" cart_id="1" item_id="0" price="10.99"
         quantity="1" /> 
</Carts>

where

  • rec_id is a unique record identifier
  • cart_id is an identifier of the cart that contains this item
  • id is an item identifier
  • price is the item price
  • quantity is the quantity of ordered items

For real online shops, information about paid carts is not stored there, but is written into the order table of the DB. But, to simplify the process and to let you easily track the payment results, I did not implement this capability in the given code. Besides, real online shops should register users to be able to identify them and create carts that can be accessed only by their users. The given example does not use registration, so it does not control access of different users to carts. This process is simplified because the user needs only to select his cart identifier.

To store information about payment requests, I use the PaymentRequests.xml file with the following structure:

<Requests>
   <Request request_id="0" cart_id="1" price="10.99"
            request_date="5/28/2007 1:15:18 PM" />
</Requests>

where

  • request_id is a unique request identifier
  • cart_id is an identifier of the cart being paid
  • price is the cost of goods
  • request_date is the date and the time when the request is created

In real online shops, information about payment requests contains an identifier of payment details from the table of payment details. But to simplify the code, I did not use this.

To store information about responses to payment requests, I use the PaymentResponses.xml file that has the following structure:

<Responses>
   <Response   payment_id="0"
               txn_id="3PP58082BD3079037"
               payment_date="5/28/2007 1:22:40 PM"
               payment_price="10.99"
               email= my@email.com 
               first_name=""
               last_name=""
               street=""
               city=""
               state=""
               zip=""
               country=""
               request_id="0"
               is_success="True"
               reason_fault=""
   />
</Responses>

where

  • payment_id is a unique payment identifier
  • txn_id is a unique number of the PayPal transaction
  • payment_date is the date and time when payment is performed
  • payment_price is the payment amount
  • email is the buyer's e-mail
  • first_name is the buyer's first name
  • last_name is the buyer's last name
  • street is the buyer's street
  • city is the buyer's city
  • state is the buyer's state
  • zip is the buyer's ZIP code
  • country is the buyer's country
  • request_id is an identifier of the payment request
  • is_success indicates whether the payment was successfully performed
  • reason_fault is a possible reason of payment failure

If you use XML files to store information, you should use the XML schema to validate the information, but I did not perform validation to simplify the example.

The form of payment request that is sent to PayPal is as follows:

<form id="payForm" method="post" action="<%Response.Write (URL)%>">
      <input type="hidden" name="cmd"
             value="<%Response.Write (cmd)%>">
      <input type="hidden" name="business"
             value="<%Response.Write (business)%>">
      <input type="hidden" name="item_name"
             value="<%Response.Write (item_name)%>">
      <input type="hidden" name="amount"
             value="<%Response.Write (amount)%>">
      <input type="hidden" name="no_shipping"
             value="<%Response.Write (no_shipping)%>">
      <input type="hidden" name="return"
             value="<%Response.Write (return_url)%>">
      <input type="hidden" name="rm"
             value="<%Response.Write (rm)%>">
      <input type="hidden" name="notify_url"
             value="<%Response.Write (notify_url)%>">
      <input type="hidden" name="cancel_return"
             value="<%Response.Write (cancel_url)%>">
      <input type="hidden" name="currency_code"
             value="<%Response.Write (currency_code)%>">
      <input type="hidden" name="custom"
             value="<%Response.Write (request_id)%>">
</form>

where

  • URL is the URL to work with, depending on whether the sandbox or a real PayPal account should be used
  • cmd is a command that is sent to PayPal
  • business is the seller's e-mail
  • item_name is the item name (what the buyer pays for) that will be shown to the user
  • amount is the payment amount
  • no_shipping is a parameter that determines whether the delivery address should be requested
  • return_url is the URL the buyer will be redirected to when payment is successfully performed
  • rm is a parameter that determines the way information about a successfully finished transaction will be sent to the script specified in the return parameter
  • notify_url is the URL PayPal will send information about the transaction (IPN) to
  • cancel_url is the URL the buyer is redirected to when he cancels payment
  • currency_code is the currency code
  • request_id is an identifier of payment request

Values of the variables are set in the PayPal.aspx.cs (or in the PayPal.aspx.vb) file of the source code attached to the article. Refer to Table 1 for a more detailed description of the form's fields.

When the request_id is passed in the custom field, it allows the IPN script to restore information about the cart. If the buyer cancels payment, he is redirected to cancel_url. But, if he performs the payment, he is redirected to return_url and, in this case, you can test interaction with PayPal, check whether the payment was performed, create a payment report, and thank the buyer for the purchase. As for the given example, use the code of IPN processing in the payment_success.aspx.cs (or in payment_success.aspx.vb) file only for testing because real products should validate payments in the IPN script specified in the notify_url parameter for security purposes. The payment_success.aspx.cs (or payment_success.aspx.vb) code was specially written to make the testing process give as much information as possible. The code contains messages that are important only at the testing stage. This information is written into a log file. The file stores not only critical errors, but also the errors that allow the site to keep working.

In general, error messages should be properly handled, but not shown to users as exceptions. The Response.Write() construction is not a good idea either. Real sites usually create a special page where information about the error is sent. Then, the information is formatted and shown to the user. For example, the user should be redirected to this page if an exception is thrown or a page requested from the site is absent. To simplify the given example, the code writes information about most errors that occur to the log file.

The return parameter is useful because, when the payment is performed, it allows the result of the verification to be shown to the user. However, the verification does not provide a 100% guarantee that the payment was really put into the seller's account. For example, if the buyer uses an e-check, the payment will be put into the seller's account only after the check is processed in a bank; that also does not provide a guarantee that the money is put into the account. That's why real online shops should use IPN and work with payment, check the payment, and protocol it in the code of the IPN script. Besides, the content of the form should be encrypted before it's sent to avoid forgery of the payment information. That means that so-called Encrypted Website Payments should be used. If you are not going to use Encrypted Website Payments (EWP) validation, you must check the price, transaction ID, PayPal receiver e-mail address, and other data sent to you by IPN to ensure that they are correct. By examining the data, you can be sure that you are not being spoofed.

Using the PayPal Payment System in ASP.NET

KB_Soft Group uses in its projects both EWP and validation of the parameters received from PayPal that provides the duplicated validation check and rules out any possibility of information forgery. To simplify the given example of an online shop and to make it work on any PayPal account, you use only IPN because if you use EWP, you have to create private and public keys and upload the created public key to your account on the PayPal server. Then, you need to use the obtained identifier of the certificate to encrypt the form of the request. Besides, to use EWP, you need to download a public key of the PayPal system itself. This article is not aimed at describing in detail the principles of working with EPW, so you can visit the PayPal site to find detailed information on this issue.

The code of the Page_Load procedure of the IPNHandler class is given below. You can find detailed information in the archives with source code attached to the article.

C#

private void Page_Load(object sender, EventArgs e)
    {
        string requestUriString;
        CultureInfo provider = new CultureInfo("en-us");
        string requestsFile = this.Server.MapPath(
            "~/App_Data/PaymentRequests.xml");
        requests.Clear();
        if (System.IO.File.Exists(requestsFile))
        {
            requests.ReadXml(requestsFile);
        }
        else
        {
            Carts.CreateXml(requestsFile, "Requests");
            requests.ReadXml(requestsFile);
        }
        string responseFile = this.Server.MapPath(
            "~/App_Data/PaymentResponses.xml");
        responses.Clear();
        if (System.IO.File.Exists(responseFile))
        {
            responses.ReadXml(responseFile);
        }
        else
        {
            Carts.CreateXml(responseFile, "Responses");
            responses.ReadXml(responseFile);
        }
        string strFormValues = Encoding.ASCII.GetString(
            this.Request.BinaryRead(this.Request.ContentLength));

        // getting the URL to work with
        if (String.Compare(
            ConfigurationManager.AppSettings["UseSandbox"].ToString(),
            "true", false) == 0)
        {
            requestUriString = 
                "https://www.sandbox.paypal.com/cgi-bin/webscr";
        }
        else
        {
            requestUriString = "https://www.paypal.com/cgi-bin/webscr";
        }

        // Create the request back
        HttpWebRequest request = 
            (HttpWebRequest)WebRequest.Create(requestUriString);

        // Set values for the request back
        request.Method = "POST";
        request.ContentType = "application/x-www-form-urlencoded";
        string obj2 = strFormValues + "&cmd=_notify-validate";
        request.ContentLength = obj2.Length;

        // Write the request back IPN strings
        StreamWriter writer = 
            new StreamWriter(request.GetRequestStream(), Encoding.ASCII);
        writer.Write(RuntimeHelpers.GetObjectValue(obj2));
        writer.Close();

        //send the request, read the response
        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        Stream responseStream = response.GetResponseStream();
        Encoding encoding = Encoding.GetEncoding("utf-8");
        StreamReader reader = new StreamReader(responseStream, encoding);

        // Reads 256 characters at a time.
        char[] buffer = new char[0x101];
        int length = reader.Read(buffer, 0, 0x100);
        while (length > 0)
        {
            // Dumps the 256 characters to a string
            string requestPrice;
            string IPNResponse = new string(buffer, 0, length);
            length = reader.Read(buffer, 0, 0x100);
            try
            {
                // getting the total cost of the goods in 
                // cart for an identifier
                // of the request stored in the "custom" variable
                requestPrice = 
                    GetRequestPrice(this.Request["custom"].ToString());
                if (String.Compare(requestPrice, "", false) == 0)
                {
                    Carts.WriteFile("Error in IPNHandler: amount = \");
                    reader.Close();
                    response.Close();
                    return;
                }
            }
            catch (Exception exception)
            {
                Carts.WriteFile("Error in IPNHandler: " + exception.Message);
                reader.Close();
                response.Close();
                return;
            }

            NumberFormatInfo info2 = new NumberFormatInfo();
            info2.NumberDecimalSeparator = ".";
            info2.NumberGroupSeparator = ",";
            info2.NumberGroupSizes = new int[] { 3 };

            // if the request is verified
            if (String.Compare(IPNResponse, "VERIFIED", false) == 0)
            {
                // check the receiver's e-mail (login is user's 
                // identifier in PayPal)
                // and the transaction type
                if ((String.Compare(this.Request["receiver_email"], 
                    this.business, false) != 0) ||
                    (String.Compare(this.Request["txn_type"], 
                    "web_accept", false) != 0))
                {
                    try
                    {
                        // parameters are not correct. Write a 
                        // response from PayPal
                        // and create a record in the Log file.
                        this.CreatePaymentResponses(this.Request["txn_id"],
                            Convert.ToDecimal(
                            this.Request["mc_gross"], info2),
                            this.Request["payer_email"], 
                            this.Request["first_name"],
                            this.Request["last_name"], 
                            this.Request["address_street"],
                            this.Request["address_city"], 
                            this.Request["address_state"],
                            this.Request["address_zip"], 
                            this.Request["address_country"],
                            Convert.ToInt32(this.Request["custom"]), false,
                            "INVALID paymetn's parameters" + 
                            "(receiver_email or txn_type)");
                        Carts.WriteFile(
                            "Error in IPNHandler: INVALID payment's" +
                            " parameters(receiver_email or txn_type)");
                    }
                    catch (Exception exception)
                    {
                        Carts.WriteFile("Error in IPNHandler: " + 
                            exception.Message);
                    }
                    reader.Close();
                    response.Close();
                    return;
                }

                // check whether this request was performed 
                // earlier for its identifier
                if (this.IsDuplicateID(this.Request["txn_id"]))
                {
                    // the current request is processed. Write 
                    // a response from PayPal
                    // and create a record in the Log file.
                    this.CreatePaymentResponses(this.Request["txn_id"],
                        Convert.ToDecimal(this.Request["mc_gross"], info2),
                        this.Request["payer_email"], 
                        this.Request["first_name"],
                        this.Request["last_name"], 
                        this.Request["address_street"],
                        this.Request["address_city"], 
                        this.Request["address_state"],
                        this.Request["address_zip"], 
                        this.Request["address_country"],
                        Convert.ToInt32(this.Request["custom"]), false, 
                        "Duplicate txn_id found");
                    Carts.WriteFile(
                        "Error in IPNHandler: Duplicate txn_id found");
                    reader.Close();
                    response.Close();
                    return;
                }

                // the amount of payment, the status of the 
                // payment, amd a possible reason of delay
                // The fact that Getting txn_type=web_accept or 
                // txn_type=subscr_payment are got odes not mean that
                // seller will receive the payment.
                // That's why we check payment_status=completed. The 
                // single exception is when the seller's account in
                // not American and pending_reason=intl
                if (((String.Compare(
                    this.Request["mc_gross"].ToString(provider), 
                    requestPrice, false) != 0) ||
                    (String.Compare(this.Request["mc_currency"], 
                    this.currency_code, false) != 0)) ||
                    ((String.Compare(this.Request["payment_status"], 
                    "Completed", false) != 0) &&
                    (String.Compare(this.Request["pending_reason"], 
                    "intl", false) != 0)))
                {
                    // parameters are incorrect or the payment 
                    // was delayed. A response from PayPal should not be
                    // written to DB of an XML file
                    // because it may lead to a failure of 
                    // uniqueness check of the request identifier.
                    // Create a record in the Log file with information 
                    // about the request.
                    Carts.WriteFile(
                        "Error in IPNHandler: INVALID paymetn's parameters."+
                        "Request: " + strFormValues);
                    reader.Close();
                    response.Close();
                    return;
                }
                try
                {
                    // write a response from PayPal
                    this.CreatePaymentResponses(this.Request["txn_id"],
                        Convert.ToDecimal(this.Request["mc_gross"], info2),
                        this.Request["payer_email"], 
                        this.Request["first_name"],
                        this.Request["last_name"], 
                        this.Request["address_street"],
                        this.Request["address_city"], 
                        this.Request["address_state"],
                        this.Request["address_zip"], 
                        this.Request["address_country"],
                        Convert.ToInt32(this.Request["custom"]), true, "");
                    Carts.WriteFile(
                        "Success in IPNHandler: PaymentResponses created");

                    ///////////////////////////////////////////////////
                    // Here we notify the person responsible for 
                    // goods delivery that 
                    // the payment was performed and providing 
                    // him with all needed information about
                    // the payment. Some flags informing that 
                    // user paid for a services can be also set here.
                    // For example, if user paid for registration 
                    // on the site, then the flag should be set 
                    // allowing the user who paid to access the site
                    //////////////////////////////////////////////////
                }
                catch (Exception exception)
                {
                    Carts.WriteFile(
                        "Error in IPNHandler: " + exception.Message);
                }
            }
            else
            {
                Carts.WriteFile(
                    "Error in IPNHandler. IPNResponse = 'INVALID'");
            }
        }
        reader.Close();
        response.Close();
    }

Visual Basic

Private Sub Page_Load(ByVal sender As System.Object, _
   ByVal e As System.EventArgs) Handles MyBase.Load

   Dim ci As CultureInfo = New CultureInfo("en-us")
   Dim requestsFile As String = _
      Server.MapPath("~/App_Data/PaymentRequests.xml")
   requests.Clear()

   If File.Exists(requestsFile) Then
      requests.ReadXml(requestsFile)
   Else
      KBSoft.Carts.CreateXml(requestsFile, "Requests")
      requests.ReadXml(requestsFile)
   End If

   Dim responseFile As String = _
      Server.MapPath("~/App_Data/PaymentResponses.xml")
   responses.Clear()

   If File.Exists(responseFile) Then
      responses.ReadXml(responseFile)
   Else
      KBSoft.Carts.CreateXml(responseFile, "Responses")
      responses.ReadXml(responseFile)
   End If

   Dim strFormValues As String = _
      Encoding.ASCII.GetString(Request.BinaryRead _
         (Request.ContentLength))
   Dim strNewValue

   ' getting the URL to work with
   Dim URL As String
   If AppSettings("UseSandbox").ToString = "true" Then
      URL = "https://www.sandbox.paypal.com/cgi-bin/webscr"
   Else
      URL = "https://www.paypal.com/cgi-bin/webscr"
   End If

   ' Create the request back
   Dim req As HttpWebRequest = _
      CType(WebRequest.Create(URL), HttpWebRequest)


   ' Set values for the request back
   req.Method = "POST"
   req.ContentType = "application/x-www-form-urlencoded"
   strNewValue = strFormValues + "&cmd=_notify-validate"
   req.ContentLength = strNewValue.Length

   ' Write the request back IPN strings
   Dim stOut As StreamWriter = _
      New StreamWriter(req.GetRequestStream(), _
   Encoding.ASCII)
   stOut.Write(strNewValue)
   stOut.Close()

   'send the request, read the response
   Dim strResponse As HttpWebResponse = _
      CType(req.GetResponse(), HttpWebResponse)
   Dim IPNResponseStream As Stream = strResponse.GetResponseStream
   Dim encode As Encoding = System.Text.Encoding.GetEncoding("utf-8")
   Dim readStream As New StreamReader(IPNResponseStream, encode)

   Dim read(256) As [Char]
   ' Reads 256 characters at a time.
   Dim count As Integer = readStream.Read(read, 0, 256)

   While count > 0
      ' Dumps the 256 characters to a string
      Dim IPNResponse As New [String](read, 0, count)
      count = readStream.Read(read, 0, 256)
      Dim amount As String
      Try
         ' getting the total cost of the goods in cart for an
         ' identifier of the request stored in the "custom"
         ' variable
         amount = GetRequestPrice(Request("custom").ToString)
         If amount = "" Then
            KBSoft.Carts.WriteFile("Error in IPNHandler: _
               amount = """)
            readStream.Close()
            strResponse.Close()
            Return
         End If

      Catch ex As Exception
         KBSoft.Carts.WriteFile("Error in IPNHandler: " + ex.Message)
         readStream.Close()
         strResponse.Close()
         Return
      End Try

      Dim provider As NumberFormatInfo = New NumberFormatInfo()
      provider.NumberDecimalSeparator = "."
      provider.NumberGroupSeparator = ","
      provider.NumberGroupSizes = New Integer() {3}

      ' if the request is verified
      If IPNResponse = "VERIFIED" Then
         ' check the receiver's e-mail (login is user's identifier
         ' in PayPal) and the transaction type
         If Request("receiver_email") <> business Or _
            Request("txn_type") <> "web_accept" Then
            Try
               ' parameters are not correct. Write a response from
               ' PayPal and create a record in the Log file.
               CreatePaymentResponses(Request("txn_id"), _
               Convert.ToDecimal(Request("mc_gross"), provider), _
               Request("payer_email"), Request("first_name"), _
               Request("last_name"), Request("address_street"), _
               Request("address_city"), Request("address_state"), _
               Request("address_zip"), Request("address_country"), _
               Convert.ToInt32(Request("custom")), False, _
               "INVALID paymetn's parameters (receiver_email _
                                              or txn_type)")
               KBSoft.Carts.WriteFile("Error in IPNHandler: _
               INVALID paymetn's parameters (receiver_email or _
                                             txn_type)")
            Catch ex As Exception
               KBSoft.Carts.WriteFile("Error in IPNHandler: " + _
                                      ex.Message)
            End Try
            readStream.Close()
            strResponse.Close()
            Return
         End If
         ' check whether this request was performed earlier for its
         ' identifier
         If IsDuplicateID(Request("txn_id")) Then
            ' the current request is processed. Write a response
            ' from PayPal and create a record in the Log file.
            CreatePaymentResponses(Request("txn_id"), _
            Convert.ToDecimal(Request("mc_gross"), provider), _
            Request("payer_email"), Request("first_name"), _
            Request("last_name"), Request("address_street"), _
            Request("address_city"), Request("address_state"), _
            Request("address_zip"), Request("address_country"), _
            Convert.ToInt32(Request("custom")), False, _
                            "Duplicate txn_id found")
            KBSoft.Carts.WriteFile("Error in IPNHandler: _
                                   Duplicate txn_id found")
            readStream.Close()
            strResponse.Close()
            Return
         End If
         ' the amount of payment, the status of the payment, amd a
         ' possible reason of delay
         ' The fact that Getting txn_type=web_accept or
         ' txn_type=subscr_payment are got odes not mean that
         ' seller will receive the payment.
         ' That's why we check payment_status=completed. The single
         ' exception is when the seller's account in not American
         ' and pending_reason=intl
         If Request("mc_gross").ToString(ci) <> amount Or _
         Request("mc_currency") <> currency_code Or _
         (Request("payment_status") <> "Completed" And _
         Request("pending_reason") <> "intl") Then
            ' parameters are incorrect or the payment was delayed.
            ' A response from PayPal should not be written to DB
            ' of an XML file because it may lead to a failure of
            ' uniqueness check of the request identifier.
            ' Create a record in the Log file with information
            ' about the request.
            KBSoft.Carts.WriteFile("Error in IPNHandler: _
            INVALID paymetn's parameters. Request: " + strFormValues)
            readStream.Close()
            strResponse.Close()
            Return
         End If

         Try
            ' write a response from PayPal
            CreatePaymentResponses(Request("txn_id"), _
            Convert.ToDecimal(Request("mc_gross"), provider), _
            Request("payer_email"), Request("first_name"), _
            Request("last_name"), Request("address_street"), _
            Request("address_city"), Request("address_state"), _
            Request("address_zip"), Request("address_country"), _
            Convert.ToInt32(Request("custom")), True, "")
            KBSoft.Carts.WriteFile("Success in IPNHandler: _
            PaymentResponses created")
            '''''''''''''''''''''''''''''''''''''''''''''''''''''''
            ' Here we notify the person responsible for goods
            ' delivery that the payment was performed and
            ' providing him with all needed information about the
            ' payment. Some flags informing that the user paid for
            ' a service can be also set here.
            ' For example, if user paid for registartion on the
            ' site, then the flag should be set, allowing the
            ' user who paid to access the site
            '''''''''''''''''''''''''''''''''''''''''''''''''''''''

         Catch ex As Exception
            KBSoft.Carts.WriteFile("Error in IPNHandler: " + _
                                   ex.Message)
         End Try

      Else
         KBSoft.Carts.WriteFile("Error in IPNHandler. _
                                IPNResponse = 'INVALID'")
      End If
   End While

   readStream.Close()
   strResponse.Close()
End Sub

Parameters of web.config Adjustment

To use the source code attached to the article, you need the web.config parameters to be correctly specified. When adjusting the parameters in the web.config file, you should pay special attention to the appSettings settings:

<appSettings>
   <!-- PayPay parameters-->
   <add key="BusinessEmail" value="mymail@mail.com"/>
   <add key="CancelPurchaseUrl" 
        value="http://YOUR_IP/paypal/default.aspx"/>
   <add key="ReturnUrl"
        value="http://YOUR_IP/paypal/payment_success.aspx"/>
   <add key="NotifyUrl"
        value="http://YOUR_IP/paypal/IPNHandler.aspx"/>
   <add key="CurrencyCode" value="USD"/>
   <add key="UseSandbox" value="true"/>
   <add key="SendToReturnURL" value="true"/>
</appSettings> 

Specify the e-mail of the payment recipient in the BusinessEmail parameter. This can be the e-mail that was used when creating an account on PayPal or an alternative e-mail specified in the account parameters as the alternative e-mail. The e-mail that is used as a login to the PayPal account usually is specified in this parameter. As for the CancelPurchaseUrl, ReturnUrl, and NotifyUrl parameters, in YOUR_IP you need to specify your global IP address (not your IP address in your local network) or a domain of the registered site on which the seller's site is hosted. While testing, you simply may specify localhost instead of YOUR_IP. It should be noted that IPN will function only if you correctly specify the global IP address or a name of the registered domain. If you specify localhost, only the script specified in the ReturnUrl parameter will be able to support interaction with PayPal, but not the script specified in the NotifyUrl parameter. You should keep in mind that IPN may be blocked depending on the firewall settings.

The links above are given for the "paypal" virtual directory. If you use another virtual directory or site, you need to specify its name instead of "paypal". To check whether your NotifyUrl is valid, enter its address into the "Notification URL:" filed at Profile->Instant Payment Notification Preferences->Edit in your PayPal Business or Premier account, check the checkbox on this page, and click the "Save" button. If the URL is valid, a message about successful validation will be shown.

The CurrencyCode parameter is used to specify the code of the currency that is used for payment. The UseSandbox parameter is used to switch between PayPal SandBox and real PayPal accounts. The SendToReturnURL parameter is used to turn on/off notifications sending to return_url. SendToReturnURL is recommended to be set to true only for the purposes of testing.

Using the Source Code

Archives with the source code of a simplified online shop are attached to the article. You can use this code in your software products to support interaction with PayPal when you need to perform payments.

To use the code supplied with the article, you need to have .NET Framework 2.0 or higher installed in your system. Additionally, you need to have a real or SandBox PayPal account. No additional requirements should be met to open a SandBox account. But, to create a real PayPal account, you need to have a credit card and/or an account in any American bank. Make sure that the standard WebClient service is running in your system. If you have Visual Studio 2005, you can run the test by opening the paypal.sln solution and executing the code. IPN will not be available in this case. An alternative option is to create a virtual directory on the IIS server.

Conclusion

In conclusion, I would like to give you some advice:

  • Nver trust the data obtained by the IPN script before you receive the VERIFIED response from PayPal. Information about processed transactions should be kept. So, when the VERIFIED response is received, you can make sure that the transaction was not processed before.
  • Do not use payer_email to identify buyers because the e-mail can be changed. Use payer_id.
  • txn_type=web_accept does not mean that the seller will receive payment. You should always check whether payment_status=completed occurred. The single exception is a seller whose account is not American and pending_reason=intl.
  • Failures are not impossible in any system, and PayPal is not an exception. If the IPN script receives suspect data, it should be written to the log and the administrator should be informed about this. It is also useful to implement a form for users to be able to inform you about errors.

References

History

  • 27 June, 2007: Added the C# implementation of the example described in article (see here).


About the Author

Anton Zlobin

Anton Zlobin is a .NET developer at KB_Soft Group, an offshore software development company located in Russia, Novosibirsk. Here he has worked on various .NET projects. He has a Master Degree in Computer Technology from Novosibirsk State Technical University.

Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Managing your company's financials is the backbone of your business and is vital to the long-term health and viability of your company. To continue applying the necessary financial rigor to support rapid growth, the accounting department needs the right tools to most efficiently do their job. Read this white paper to understand the 10 essentials of a complete financial management system and how the right solution can help you keep up with the rapidly changing business world.

  • Live Event Date: August 14, 2014 @ 2:00 p.m. ET / 11:00 a.m. PT Data protection has long been considered "overhead" by many organizations in the past, many chalking it up to an insurance policy or an extended warranty you may never use. The realities of today makes data protection a must-have, as we live in a data-driven society -- the digital assets we create, share, and collaborate with others on must be managed and protected for many purposes. Check out this upcoming eSeminar and join Seagate Cloud …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds