Build an e-ticketing system with PayPal, Python and Google App Engine

By Peter Georgeson


In this article we demonstrate how to build an e-ticketing application with some common technologies, namely:

Even in today's modern world, it is still quite common when purchasing a ticket to an event, to have to print it out, or even receive it via the post (!) You then take this paper ticket to the event to gain entry. This quaint process needn't be the case with today's technological advances.

This article demonstrates how this process might be modernized. One should be able to purchase a ticket online with your mobile device, then use your mobile to gain entrance to the event. We make use of PayPal for payment, and QR codes for the scanning and authentication process.

The source code associated with this article is available at GitHub and is based on an existing App Engine Shopping Cart. With just a few modifications to an existing shopping cart, an e-ticketing solution can be deployed.

Google App Engine

Google App Engine is a fast and free way to get a web based application online.

The server side of this solution was implemented as a Google App Engine application and is in the server directory of the source repository.

Deploying a GAE application is not covered in detail here, however, the basic steps are:


If you intend on running the sample application, it is highly recommended that you sign up for a PayPal sandbox account at and request buyer and seller sandbox credentials.

This example builds on an existing Python based PayPal library and uses the existing workflow of the original App Engine shopping cart. The status of a purchase is stored on the model.Purchase table. A typical purchase process has the following workflow:

This system allows both the completed and returned statuses to indicate a successful transaction, however, technically only completed can be considered to be a true indication of a successful transaction.

The main reason that this implementation allows completed is to allow the application to be tested locally, running on localhost. IPN requires an externally accessible server for PayPal to send the notifications to.

In a more robust implementation, the returning user would only be shown the QR code if the completed status had been set, indicating that an IPN had been received from PayPal.

If a user returned to the shopping cart without the IPN arriving first, a more robust implementation would indicate to the buyer that their transaction was pending and that they would be notified of the success of their transaction. Implementing the workflow in this more robust manner is reserved as an exercise for the reader, and may be covered in future articles.

In a normal shopping cart, the next stage would be for the purchased item to be shipped if it was a physical item, or made available for download in the case of a digital purchase. In this case, we want to provide the user with a QR code that can be scanned at the event to gain entry.

Customizing the shopping cart for e-tickets

With a few changes, the App Engine shopping cart can be updated to support an e-ticketing system.

Item Expiry

Firstly, an expiry is added to model.Item. This allows events that have already occurred to be excluded from the displayed results. It also allows the application to display more relevant results by displaying upcoming events. This is implemented with GQL (Google Query Language) in model.Item.soon():

return Item.all().filter( "enabled =", True ).filter( "expiry >", ).order('expiry').fetch(10)

This query returns a list of enabled items that have an expiry after today, ordered such that those expiring soonest are shown first.

Figure 1: The new home page, filled with exciting upcoming events

Purchase Code

Secondly, after a successful purchase, we want to provide the buyer with a QR code, and as a fallback, a purchase code that can also be used at entry for proof of purchase. For this purpose a code field was added to model.Purchase.

The field is populated with a random 6 character case-insensitive alphabetic code when the purchase instance is first created:

purchase = model.Purchase( item=item, owner=item.owner, purchaser=users.get_current_user(), status='NEW', secret=util.random_alnum(16), code=util.random_lowercase(6) )

Potentially this code would be manually checked when entering the event, so the code needs to be relatively short and easy to enter.

Having only 6 case insensitive letters might not seem particularly secure, however, this provides approximately 300 million combinations (266). So at a really big event with, for example, 100000 tickets, an attacker could expect to need 3000 guesses on average before picking a valid code.

Although 3000 guesses might be quite feasible when attempting to compromise someone's login details on a web site, entering an event isn't typically a situation where you have the opportunity to make 3000 guesses at a valid code. The people standing behind you would likely stymie your illegal code guessing efforts after the first few hundred attempts.

Success Page

With the purchase code generated, we can provide the user with the QR code based on this purchase code, as well as the actual code in case manual verification is necessary.

Google Charts provide a QR code generator, which is utilized in this method on model.Purchase:

  def qr_code( self ):
    return "" % self.code

To serve this purpose, a dedicated "success" page was created at templates/success.htm and main.BuyReturn modified to return the user to this page. Now we can display the QR code and purchase code.

Present the QR code below, or quote your purchase ID, which is <strong>{{ purchase.code }}</strong>.
<img src="{{ purchase.qr_code }}"/>

Figure 2: Entry ticket: obtained

User Roles

The existing App Engine application allows any user to be a seller, a-la eBay. In this application, we restrict event sellers to those users that have been explicitly given sell permission. To enforce these permissions, a role field was added to model.Profile, along with an is_seller method:

  def is_seller( u ):
    if u == None:
      return False
    profile = Profile.from_user( u )
    return profile != None and profile.role == 'seller'

Now this role is checked before allowing a user to enter the sell pages:

class SellHistory (RequestHandler):
  def get(self):
    if not model.Profile.is_seller( users.get_current_user() ):
With this checking in place, if you wish to use the seller functionality you will need to manually edit the Profile table to give your user the 'seller' role.

This functionality could be extended to provide a more complete role checking implementation.

Purchase History

After purchasing a ticket, it might be annoying to be required to keep your browser open until the event, just so that you can have the QR code ready. Consequently, a purchase history page was added to the application. This way, even if the buyer closes their browser or even shuts down their computer, they can still find their purchase codes and successfully negotiate entry into their favorite concert.

A new request handler, main.BuyHistory was added to

class BuyHistory (RequestHandler):
  def get(self):
    data = {
      'items': model.Purchase.all().filter( 'purchaser =', users.get_current_user() ).order('-created').fetch(100),
    util.add_user( self.request.uri, data )
    path = os.path.join(os.path.dirname(__file__), 'templates/buyhistory.htm')
    self.response.out.write(template.render(path, data))
app = webapp.WSGIApplication( [
   ('/buyhistory', BuyHistory),
Now templates/buyhistory.htm simply iterates over these purchased items and renders the necessary details.


This tutorial demonstrated how an existing shopping cart application could be customized to implement a slightly different use case - an e-ticketing system.

This part of the tutorial implemented the purchase process and focused on the web server.

The next part implements the "redemption" part of the process - when a user redeems a ticket. A seller needs an easy, mobile method of checking for valid purchasers - this is demonstrated in part 2 of the series.

Please note that this application should not be considered "production-ready" - it's a demonstration of how easy it is to leverage these technologies and build interesting applications. Use at your own risk.

Further Details