PayPal with Flask Web Development Tutorial




In this Flask web development tutorial with Python, we're going to cover how to integrate PayPal. When I first started poking around how to implement PayPal automatically in some way, I was quickly overwhelmed with options, and various services. There's the legacy PayPal API, Braintree, and the IPN. Braintree does appear to have some pretty cool options, especially if you purely want to use PayPal is a payment processor, and nothing more, but, for most mere mortals, they really just want to have a business PayPal, with maybe a store that has some buy with Paypal buttons, or maybe a subscription. For this task, all we really need is to speak with the IPN (instant payment notification).

The IPN is fairly straightforward. It will notify you about just about everything involved in the transaction. With a subscription, for example, the IPN will notify you when:

A new payment plan is created
When a payment is made
When a payment is late
When someone cancels their subscription plan
...and probably more that I am forgetting.

These are the identical notifications that are also sent to your PayPal email, which you may be using manually right now! As such, communicating with the IPN is all you need to automate PayPal sales. You may need to enable the IPN with your account. To do this, log into Paypal, head to "my profile," then choose settings. Next, within "My selling tools," look for "Instant payment notifications" and the update button. Click on that, and make sure Message delivery: Enabled. You can also fill in the notification URL, but that wont be required for the method we're going to use.

There are a lot of moving parts when you go to create a store or subscription system, however. Chances are, you will need to do a lot of testing and debugging. You could do it between two real PayPal accounts, just refunding the money until you get it all set, but PayPal actually has quite a complex "sandbox." The Sandbox acts just like PayPal does, but it allows you try all sorts of things with fake money. When you're ready to go live, PayPal actually does still recommend you test live with real accounts at least once to make sure all is well.

To play in the sandbox, you will need a developer account, head to the PayPal developer area. Create an account, and then log in. Once in, head to the dashboard. If I recall right, there are already two Accounts for you. If not, you will want to make them. The default should be that you have two accounts with very similar emails as you do, only with -facilitator and -buyer appended to them. You will be able to log into the buyer account, for example, on Sandbox.Paypal.com, and it will be just like the real thing. If only there was a way to transfer that balance! What you will be able to do is add a button on your development site that is part of the sandbox, and then you can actually pay with your sandbox buyer account. From there, you should be able to check the seller's account, as well as also having the sandbox's IPN speaking to your program, and your program acting however you set. When you are ready to go live, you just simply remove the sandbox subdomain from the links, change the business email, and you're good to go.

To start, we are going to need three main pages: a purchase page, IPN page/handler, and a success page.. The purchase page will contain some sort of subscription or purchase button. You can generate these on Paypal, but you can also just create them on your own. PayPal buttons are ugly and unprofessional looking, so we'll make our own. The IPN handler is what will handle the notifications, storing the results to a database and otherwise performing any operations we need immediately. The success page will come after everything else. Something like "thanks for subscribing" or something of that sort. Let's make a quick purchase page and success page:

__init__.py
@app.route('/purchase/')
def purchase():
	try:
		return render_template("subscribe.html")
	except Exception, e:
		return(str(e))

templates/subscribe.html
{% extends "header.html" %}
{% block body %}
<body class="body">
	<div class="container">
		{% if session['logged_in'] %}
			<form name="topaypal" action="https://www.sandbox.paypal.com/cgi-bin/webscr" method="post">
				<input type="hidden" name="cmd" value="_xclick-subscriptions">
				<input type="hidden" name="custom" value="{{session['username']}}"/>
				<input type="hidden" name="business" value="harrison-facilitator@pythonprogramming.net">
				<input type="hidden" name="item_name" value="subscription button">
				<input type="hidden" name="item_number" value="500">
				<input type="hidden" name="no_shipping" value="1">
				<input type="hidden" name="a3" value="10.00">
				<input type="hidden" name="p3" value="1">
				<input type="hidden" name="t3" value="M">
				<input type="hidden" name="src" value="1">
				<input type="hidden" name="sra" value="1">
				<input type="hidden" name="return" value="http://104.236.221.91/success/">
				<input type="hidden" name="cancel_return" value="http://104.236.221.91/">
				<input type="hidden" name="notify_url" value="http://104.236.221.91/ipn/">
				<input type="submit" value="Subscribe" name="submit" title="PayPal - The safer, easier way to pay online!" class="btn btn-primary">
			</form>	
		{% else %}
			<p>You need to be <a href="/login/" target="blank"><strong>logged in</strong></a> to subscribe.</p>
		{% endif %}
	</div>
</body>
{% endblock %}

Here, notice all the hidden inputs. Each of these are parameters for PayPal buttons. The only custom one I am adding here is <input type="hidden" name="custom" value="{{session['username']}}"/>. This grabs the username, but it may be wiser to actually grab the user ID instead, especially if you plan to ever allow users to change their username. That said, you could run an UPDATE query in the end anyway. Back on topic, however. There are quite a few options here. Note the return, which is a custom return page on successful purchase, there's also a cancel_return option. The main crux here is the notify_url, however. This is the URL that PayPal will send the IPN towards.


__init__.py
@app.route('/success/')
def success():
	try:
		return render_template("success.html")
	except Exception, e:
		return(str(e))

templates/success.html
{% extends "header.html" %}
{% block body %}
<body class="body">
	<div class="container">
		<p>Thanks for your purchase!</p>
	</div>
</body>
{% endblock %}

Now for handling the IPN. We'll take it in parts:

__init__.py
from werkzeug.datastructures import ImmutableOrderedMultiDict
import requests

#... other code ...#

@app.route('/ipn/',methods=['POST'])
def ipn():
	try:
		arg = ''
		request.parameter_storage_class = ImmutableOrderedMultiDict
		values = request.form
		for x, y in values.iteritems():
			arg += "&{x}={y}".format(x=x,y=y)

		validate_url = 'https://www.sandbox.paypal.com' \
					   '/cgi-bin/webscr?cmd=_notify-validate{arg}' \
					   .format(arg=arg)
		r = requests.get(validate_url)

We start off by taking the post, and building a request that we'll make back to PayPal. What is happening here is: Someone has accessed our IPN page and submitted some values, but we currently do not have any good reason to believe that this came from PayPal necessarily. Thus, we build a request, and submit it to PayPal to see if PayPal agrees with the exchange. Note that we are using the sandbox link here too. From here, we make the request to PayPal, hoping for a return saying VERIFIED.

@app.route('/ipn/',methods=['POST'])
def ipn():
	try:
		arg = ''
		request.parameter_storage_class = ImmutableOrderedMultiDict
		values = request.form
		for x, y in values.iteritems():
			arg += "&{x}={y}".format(x=x,y=y)

		validate_url = 'https://www.sandbox.paypal.com' \
					   '/cgi-bin/webscr?cmd=_notify-validate{arg}' \
					   .format(arg=arg)
		r = requests.get(validate_url)
		if r.text == 'VERIFIED':
			try:
				payer_email =  thwart(request.form.get('payer_email'))
				unix = int(time.time())
				payment_date = thwart(request.form.get('payment_date'))
				username = thwart(request.form.get('custom'))
				last_name = thwart(request.form.get('last_name'))
				payment_gross = thwart(request.form.get('payment_gross'))
				payment_fee = thwart(request.form.get('payment_fee'))
				payment_net = float(payment_gross) - float(payment_fee)
				payment_status = thwart(request.form.get('payment_status'))
				txn_id = thwart(request.form.get('txn_id'))
			except Exception as e:
				with open('/tmp/ipnout.txt','a') as f:
					data = 'ERROR WITH IPN DATA\n'+str(values)+'\n'
					f.write(data)
			
			with open('/tmp/ipnout.txt','a') as f:
				data = 'SUCCESS\n'+str(values)+'\n'
				f.write(data)

			c,conn = connection()
			c.execute("INSERT INTO ipn (unix, payment_date, username, last_name, payment_gross, payment_fee, payment_net, payment_status, txn_id) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)",
						(unix, payment_date, username, last_name, payment_gross, payment_fee, payment_net, payment_status, txn_id))
			conn.commit()
			c.close()
			conn.close()
			gc.collect()

		else:
			 with open('/tmp/ipnout.txt','a') as f:
				data = 'FAILURE\n'+str(values)+'\n'
				f.write(data)
				
		return r.text
	except Exception as e:
		return str(e)

If it is verified, then we proceed to pull the values from the IPN submission. If all goes well, we insert all of the relevant data to a table called ipn in our database. If not verified, then we log this to a file in /tmp for further review. For our final task, we need to actually create our database table. Log into MySQL mysql --user=root -p, access your table, mine is pythonprogramming, so USE pythonprogramming;, now we create a new table: CREATE TABLE ipn (unix INT(10), payment_date VARCHAR(30), username VARCHAR(20), last_name VARCHAR(30), payment_gross FLOAT(6,2), payment_fee FLOAT(6,2), payment_net FLOAT(6,2), payment_status VARCHAR(15), txn_id VARCHAR(25));

Now comes the moment of truth! If you forgot the buyer sandbox account, log back into developer.paypal.com, go to your dashboard, then sandbox, and grab the email and password. If you forgot the password, make a new one, and we're ready to rumble! Quit MySQL quit; and reload apache service apache2 reload, and make your way to /purchase/. Click the button, log in with the sandbox PayPal account, agree, and pay. If you click to return back to the seller's website, you should wind up back on your website's success page. Now, check your database, mysql --user=root -p, then USE pythonprogramming;, and then do a SELECT * FROM ipn;. You should get a table returned like:

mysql> select * from ipn;
+------------+---------------------------+----------+-----------+---------------+-------------+-------------+----------------+-------------------+
| unix       | payment_date              | username | last_name | payment_gross | payment_fee | payment_net | payment_status | txn_id            |
+------------+---------------------------+----------+-----------+---------------+-------------+-------------+----------------+-------------------+
| 1446061985 | 12:52:10 Oct 28, 2015 PDT | Harrison | buyer     |         10.00 |        0.59 |        9.41 | Completed      | 56C78709R55237341 |
+------------+---------------------------+----------+-----------+---------------+-------------+-------------+----------------+-------------------+
1 row in set (0.00 sec)

Now you have records, you can do whatever it is you need to do. If you want to upgrade a user in something like a subscription service, when you go to update the database, if the payment is for the right amount, and the status is complete, then you could also upgrade the user on the spot right there. Take note that people CAN update the values in your form, paying you something like 0.01 USD. If you are not checking for this, you might find yourself in trouble, especially if you are shipping tangible goods!

When you believe everything to be in working order, you can do something like TRUNCATE TABLE ipn; to clear to development information, then you can go to your original form, editing in your actual business PayPal information, and changing sandbox.paypal to just paypal, same with the IPN handler.


There exists 1 quiz/question(s) for this tutorial. for access to these, video downloads, and no ads.

To learn how to secure your webserver for free with SSL, head to the next tutorial:




  • Introduction to Practical Flask
  • Basic Flask Website tutorial
  • Flask with Bootstrap and Jinja Templating
  • Starting our Website home page with Flask Tutorial
  • Improving the Home Page Flask Tutorial
  • Finishing the Home Page Flask Tutorial
  • Dynamic User Dashboard Flask Tutorial
  • Content Management Beginnings Flask Tutorial
  • Error Handling with Flask Tutorial
  • Flask Flash function Tutorial
  • Users with Flask intro Tutorial
  • Handling POST and GET Requests with Flask Tutorial
  • Creating MySQL database and table Flask Tutorial
  • Connecting to MySQL database with MySQLdb Flask Tutorial
  • User Registration Form Flask Tutorial
  • Flask Registration Code Tutorial
  • Finishing User Registration Flask Tutorial
  • Password Hashing with Flask Tutorial
  • Flask User Login System Tutorial
  • Decorators - Login_Required pages Flask Tutorial
  • Dynamic user-based content Flask Tutorial
  • More on Content Management Flask Tutorial
  • Flask CMS Concluded Flask Tutorial
  • The Crontab Flask Tutorial
  • Flask SEO Tutorial
  • Flask Includes Tutorial
  • Jinja Templating Tutorial
  • Flask URL Converters Tutorial
  • Flask-Mail Tutorial for email with Flask
  • Return Files with Flask send_file Tutorial
  • Protected Directories with Flask Tutorial
  • jQuery with Flask Tutorial
  • Pygal SVG graphs with Flask Tutorial
  • PayPal with Flask Web Development Tutorial
  • Securing your Flask website with SSL for HTTPS using Lets Encrypt