PayPal IPN Revised for Python

This article adds onto the previous entry Paypal IPN with PHP, by showing you how to process an Instant Payment Notification from Paypal with Python. For more information about setting up your PayPal account or purchase code for Instant Payment Notifications, refer to the link above.

For any python app, you will need a way to launch it from the terminal. You can use the “shebang” below to make the app self serving. You will of course need to make sure it has executable privilages (chmod +x ./app.py on most unix/linux systems). If the instance of python you are using is not the systemwide instance such as python 2.6 installed to /opt, you will then then change it to use python2.6 instead.

#!/usr/bin/env python

You still launch the app via the interpreter directly, omitting the above shebang if you’d like.

With the exception of circuits.web, all of the modules used are built into Python (as of 2.6 to my knowledge).

from time import time
from urllib import urlencode
from sqlite3 import connect, Error as sqerr
from urllib2 import urlopen, Request
from circuits.web import Controller, Server

I have created a separate method for verification to keep the rest of the code cleaner. This is pretty much the same process PayPal provides developers, plus two checks that I feel are important.

def verify_ipn(data):
	# prepares provided data set to inform PayPal we wish to validate the response
	data["cmd"] = "_notify-validate"
	params = urlencode(data)
 
	# sends the data and request to the PayPal Sandbox
	req = Request("""https://www.sandbox.paypal.com/cgi-bin/webscr""", params)
	req.add_header("Content-type", "application/x-www-form-urlencoded")
	# reads the response back from PayPal
	response = urlopen(req)
	status = response.read()
 
	# If not verified
	if not status == "VERIFIED":
		return False
 
	# if not the correct receiver ID
	if not data["receiver_id"] == "DDBSOMETHING4KE":
		return False
 
	# if not the correct currency
	if not data["mc_currency"] == "USD":
		return False
 
	# otherwise...
	return True

The verification url provided above is for the PayPal sandbox, a developer testing ground. You can signup for access to test accounts, and tools such as IPN Test submissions at PayPal SandBox. When the app is ready for production, simply remove .sandbox from the url.

Just because PayPal says it’s Verified, only means that a payment has a occured, and that the data received is the same on Paypal’s database. Someone could have paid themselves using your IPN URL; for this reason we want to make sure that the receiver_id matches your own. The Secure Merchant ID can be found on the top your Paypal account’s profile page as shown below:

Next is a currency check, because 100 Zimbewe dollars is not equivalent to 100 US Dollars. The variables mc_gross as well as item prices don’t establish the currency used, but rather simply the value amount. So this check is important to protect against incorrect purchase amounts.

Now we have the main Root class of the IPN app.

class Root(Controller):
	# if the app will not be served at the root of the domain, uncomment the next line
	#channel = "/ipn"
 
	# index is invoked on the root path, or the designated channel URI
	def index(self, **data):
		# If there is no txn_id in the received arguments don't proceed
		if not "txn_id" in data:
			return "No Parameters"
 
		# Verify the data received with Paypal
		if not verify_ipn(data):
			return "Unable to Verify"
 
		# Suggested Check : check the item IDs and Prices to make sure they match with records
 
		# If verified, store desired information about the transaction
		reference = data["txn_id"]
		amount = data["mc_gross"]
		email = data["payer_email"]
		name = data["first_name"] + " " + data["last_name"]
		status = data["payment_status"]
 
		# Open a connection to a local SQLite database (use MySQLdb for MySQL, psycopg or PyGreSQL for PostgreSQL)
		conn = connect('db')
		curs = conn.cursor()
		try:
			curs.execute("""INSERT INTO ipn (id, purchased, txn, name, email, price, notes, status) 
			VALUES (NULL, ?, ?, ?, ?, ?, NULL, ?)""", (time(), reference, name, email, amount, status,))
			conn.commit()
		except sqerr, e:
			return "SQL Error: " + e.args[0]
		conn.close()
 
		# Alternatively you can generate license keys, email users login information
		# or setup accounts upon successful payment. The status will always be "Completed" on success.
		# Likewise you can revoke user access, if status is "Canceled", or another payment error.
 
		return "Success"

In most cases you would host each application on their own domain or subdomain, such as ipn.domain.com. But I generally prefer to run all my apps from a single subdomain, such as apps.domain.com. The Controller above is expecting / for the root base, not /ipn/ like I would have. As a result we add the channel line to inform the controller of the new base.

The variable data will be received as a dict type, we’ll first check to make sure data has come in (by seeing if there’s a transaction ID with the data received), then verify the data with Paypal. Once verified, the desired information can be stored in a database, be it SQLite, MySQL, PostgreSQL or Durus.

Three things I would suggest for this portion: use a log or email to keep track of errors, and if you use sell a number of products online, check the item ID and prices to verify their accuracy. Also if your products are subscriptions, or prone to returns, check to see if a transaction ID already exists in the database and update it’s status accordingly as opposed to creating a new record for every instant payment notification, useful if someone cancels or charges back their transaction.

The next method is not very practical by itself but could lead to more useful ideas. This would be placed under the same Root class above.

	def lookup(self, id):
		if not id:
			return "No Transaction Provided"
 
		conn = connect('db')
		curs = conn.cursor()
		try:
			# Pulls a record from the database matching the transaction ID
			curs.execute("""SELECT name FROM ipn WHERE txn = ? LIMIT 1""", (id,))
			row = curs.fetchone()
			ret = row[0]
		except sqerr, e:
			ret = "SQL Error: " + e.args[0]
 
		# The response will either by the name of the buyer, or a SQL error message
		return ret

The above method would allow you to access a URL such as http://domain.com/lookup/transactionid/ or http://domain.com/lookup/?id=transactionid, and in return see the name of the buyer of that transaction.

We still need a way to serve this app to the web so that Paypal can reach it.

# Standard TCP method		
#(Server(("127.0.0.1", 9000)) + Root()).run()
 
# Unix Socket Method - make sure webserver can read and write to the socket file
(Server(("ipn.sock")) + Root()).run()

On my server I tend to prefer unix socket files as a means of connection since they’re easier to recongnize in a webserver configuration (The app name is right in the socket file name as opposed to “was it on port 9005 or 9008?”). However a standard TCP setup is more recognizable, and has wider support with most webservers. It is also important to note that using 127.0.0.1 will only allow connections from other services on the same server. To allow a service from outside the server to connect directly to the application, you will need to use a public IP address or 0.0.0.0

Notes
Using Unix Sockets
As of writing this, Unix Sockets are only supported by the development build of Circuits.web, which can be accessed with mercurial using https://dev.circuits.googlecode.com/hg/. The current 1.2 stable release can be obtained from Circuits – Google Code, or by supplying easy_install with the package ‘circuits’. An alternate web framework of similar syntax is CherryPy.

Error Outputs
Paypal always considers a response code of 200 as confirmation that the notification has been successfully delivered. In your production copy you’ll likely wish to change the output to log the errors, and send back a generic error message to the screen, possibly by creating another method.

View the next page to see the example code in uninterrupted form, as well as information on serving the example app with the Nginx webserver.

7 comments

  1. This is great! Thanks for this post. I am just starting python and this will help a lot.

  2. Carlton Oetzel says:

    Hello can I reference some of the content from this blog if I link back to you?

  3. kbeezie says:

    Sure, however I do not know what most of it has to do with free gift cards? (potential spam url was removed)

  4. I highly recommend also using dcramer’s django-payapl branch:

    http://github.com/dcramer/django-paypal

  5. kbeezie says:

    Course one would have to use django (which isn’t a bad thing if you’re already down that route).

  6. Alex says:

    I am tinkering with Paypal’s IPN, and I have a question about your code.

    According to PayPal’s docs, the POST you send back to them must contain the variables in the exact same order as they were sent to you (with cmd=_notify-validate in the beginning).

    In your code, you’re just adding a new item to the dictionary (thus you cannot predict in which order the final request will be formed). I suspect that in certain cases PayPal will refuse to accept your response, because the order of the values has changed.

    Has that ever happened to you? Or are PayPal’s rules more relaxed than the docs say?

  7. kbeezie says:

    Has not happened to me, though one would assume that if unchanged the info goes right back to them in the same order. But I’ve never encountered that problem with either code.