Login

Accepting and processing PayPal IPN messages (including using App Engine)

Author:
simon
Posted:
August 13, 2008
Language:
Python
Version:
.96
Score:
3 (after 3 ratings)

PayPal's https://www.paypal.com/ipn is ridiculously easy to consume. You can tell PayPal to POST every single transaction on your account to a URL somewhere, then set up a handler at that URL which processes those transactions in some way. Security is ensured by POSTing the incoming data back to PayPal for confirmation that the transaction is legitimate.

These classes are probably over-engineered, but they were a fun experiment in creating class-based generic views.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
"""
Classes for accepting PayPal's Instant Payment Notification messages in a 
Django application (or Django-on-App-Engine):

https://www.paypal.com/ipn

Usage:

from paypal import Endpoint  # Or AppEngineEndpoint as Endpoint

class MyEndPoint(Endpoint):
    def process(self, data):
        # Do something with valid data from PayPal - e-mail it to yourself,
        # stick it in a database, generate a license key and e-mail it to the
        # user... whatever
        
    def process_invalid(self, data):
        # Do something with invalid data (could be from anywhere) - you 
        # should probably log this somewhere

These methods can optionally return an HttpResponse - if they don't, a 
default response will be sent.

Then in urls.py:

    (r'^endpoint/$', MyEndPoint()),

"data" looks something like this:

{
    'business': '[email protected]',
    'charset': 'windows-1252',
    'cmd': '_notify-validate',
    'first_name': 'S',
    'last_name': 'Willison',
    'mc_currency': 'GBP',
    'mc_fee': '0.01',
    'mc_gross': '0.01',
    'notify_version': '2.4',
    'payer_business_name': 'Example Ltd',
    'payer_email': '[email protected]',
    'payer_id': '5YKXXXXXX6',
    'payer_status': 'verified',
    'payment_date': '11:45:00 Aug 13, 2008 PDT',
    'payment_fee': '',
    'payment_gross': '',
    'payment_status': 'Completed',
    'payment_type': 'instant',
    'receiver_email': '[email protected]',
    'receiver_id': 'CXZXXXXXQ',
    'residence_country': 'GB',
    'txn_id': '79F58253T2487374D',
    'txn_type': 'send_money',
    'verify_sign': 'AOH.JxXLRThnyE4toeuh-.oeurch23.QyBY-O1N'
}
"""
from django.http import HttpResponse
import urllib

class Endpoint:
    
    default_response_text = 'Nothing to see here'
    verify_url = "https://www.paypal.com/cgi-bin/webscr"
    
    def do_post(self, url, args):
        return urllib.urlopen(url, urllib.urlencode(args)).read()
    
    def verify(self, data):
        args = {
            'cmd': '_notify-validate',
        }
        args.update(data)
        return self.do_post(self.verify_url, args) == 'VERIFIED'
    
    def default_response(self):
        return HttpResponse(self.default_response_text)
    
    def __call__(self, request):
        r = None
        if request.method == 'POST':
            data = dict(request.POST.items())
            # We need to post that BACK to PayPal to confirm it
            if self.verify(data):
                r = self.process(data)
            else:
                r = self.process_invalid(data)
        if r:
            return r
        else:
            return self.default_response()
    
    def process(self, data):
        pass
    
    def process_invalid(self, data):
        pass

class AppEngineEndpoint(Endpoint):
    
    def do_post(self, url, args):
        from google.appengine.api import urlfetch
        return urlfetch.fetch(
            url = url,
            method = urlfetch.POST,
            payload = urllib.urlencode(args)
        ).content

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 10 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 6 months ago

Comments

simon (on August 22, 2008):

PayPal have an IPN simulation tool for testing, which you can use to send example requests to your endpoint URL:

https://developer.paypal.com/cgi-bin/devscr?cmd=_ipn-link-session

(You'll need to sign up for a PayPal developer account to use it)

Annoyingly, it doesn't look like it's possible to get that tool to send UTF8 rather than windows-1252.

#

pjs (on October 3, 2008):

Great snippet. Much cleaner than the paypalipn app I created.

One thing though, I think you should make the Endpoint class inherit 'object' at it's base..

class Endpoint(object):

So that when over writing methods like __init__ you can call super() without any problems.

For instance, I wanted to change the obj.verify_url when calling the class for testing purposes..

def __init__(self, *args, **kwargs): is_test = kwargs.pop('is_test', False) super(PaypalIPN, self).__init__(*args, **kwargs) if is_test: self.verify_url = 'https://www.sandbox.paypal.com/cgi-bin/webscr'

Without object as the base inheritance this raises a TypeError.

Thanks again!

#

aronchi (on November 11, 2008):

does anyone managed to make express checkout work?

#

pjs (on November 25, 2008):

I ran into Unicode errors when processing orders from some countries (Encode/Decode errors)..

I fixed it by replacing args.update(data) in the verify method with the following:

for k, v in data.items():
    args[k] = v.encode('utf-8')

Seemed to solve my issues..

#

davidfung (on July 17, 2013):

This snippet helps me to understand how IPN works, and eventually I am able to implement my own IPN handler (Django 1.6 on Python 2.6). Thank you.

One thing I am not sure is that how to make sure the cmd=_notify_validate is put in the beginning of the parameter list (as requested by PayPal) without the use of an OrderedDict. So I force it to the beginning by doing something similar to the following:

'cmd=_notify_validate&' + urllib.urlencode(data)

#

Please login first before commenting.