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

  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': 'your-business@example.com',
    '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': 'payer@example.com',
    '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': 'your-email@example.com',
    '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

Comments

simon (on August 22, 2008):

By default, PayPal sends IPN notification encoded using windows-1252 - which means characters used outside of Europe will get lost or mangled.

You can tell PayPal to send IPN notifications in UTF8 instead. Here's the process:

  1. Log in to your account at https://www.paypal.com/
  2. Click on the Profile subtab
  3. Click on the link "Language Encoding" under the "selling Preferences column
  4. Select the "More Options" button
  5. Using the drop down menu select your encoding preference and select "Yes" to use this preference with IPN.
  6. Click "Save"

If you don't do this, you'll need to tell Django to expect submissions in windows-1252, by setting request.encoding = 'windows-1252' at some point before the PayPal processing logic runs. It's a better idea to just tell PayPal to send UTF8 though.

#

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..

#

(Forgotten your password?)

You may use Markdown syntax here, but raw HTML will be removed.