PayPal IPN Revised for Python

Configuring with Nginx

Normally you can serve your applications directly to the web for tasks such as these, but some instances may require serving static files such as graphics or css. You can do this with circuits.web, but you can also use a webserver such as Nginx to serve both static files and your python applications. Primarily handy if you don’t wish link directly to a non-standard port.

A typical configuration for Nginx:

server {
	server_name apps.domain.com;
	root html/static_files;
	location / { 
		try_files $uri $uri/ @app;
	}
	location @app {
		proxy_set_header  X-Real-IP  $remote_addr;
		proxy_pass	http://unix:/opt/app/ipn/socket:/;
		# if you are not using a socket with your Python App use the line below instead
		# proxy_pass http://127.0.0.1:8000/;
	}
 
	location = /favicon.ico { access_log off; log_not_found off; }	
	location = /robots.txt { access_log off; log_not_found off; }
	location ~ /\. { deny  all; access_log off; log_not_found off; }
}

The try_files directive is available on Nginx 0.7.26 and above. Its a very useful directive that allows you to test the request URI a number of ways before falling back to a catch-all location. With this a static file such as css, or javascript, or a folder such as /images, would be served directly by Nginx. Anything else not caught by location rules would be sent to @app to be proxied to your Python application.

You’ll also notice a few extras on the bottom of the server configuration. To cut down on access log clutter as well as erorr log clutter if you don’t have a favicon or robots.txt file ,you can specifically turn off the logging of those types of access. Also its always a good idea to deny access to any possible .hidden unix files that may exist in your document’s root path (such as an .htpasswd copied over from another host).

Uninterrupted Source
For your convenience, the complete example source has been pasted below in uninterrupted form.

#!/usr/bin/env python
 
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
 
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
 
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"
 
	def lookup(self, id):
		if not id:
			return ierr("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 = ierr(e.args[0])
 
		# The response will either by the name of the buyer, or a SQL error message
		return ret
 
# 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()

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.