Using PayPal Standard IPN

  1. Edit and add paypal.standard.ipn to your INSTALLED_APPS:


    For installations on which you want to use the sandbox, set PAYPAL_TEST to True.

    PAYPAL_TEST = True
  2. Update the database

  3. Create an instance of the PayPalPaymentsForm in the view where you would like to collect money.

    You must fill a dictionary with the information required to complete the payment, and pass it through the initial parameter when creating the PayPalPaymentsForm.

    Please note: This form is not used like a normal Django form that posts back to a Django view. Rather it is a POST form that has a single button which sends all the data to PayPal. You simply need to call render on the instance in your template to write out the HTML, which includes the <form> tag with the correct endpoint.

    For a full list of variables that can be used in paypal_dict, see PayPal HTML variables documentation.


    The names of these variables are not the same as the values returned on the IPN object.


    <h1>Show me the money!</h1>
    <!-- writes out the form tag automatically -->
    {{ form.render }}

    The image used for the button can be customized using the Settings, or by subclassing PayPalPaymentsForm and overriding the get_image method.

  4. When someone uses this button to buy something PayPal makes a HTTP POST to your “notify_url”. PayPal calls this Instant Payment Notification (IPN). The view paypal.standard.ipn.views.ipn handles IPN processing. To set the correct notify_url add the following to your

    from django.urls import path, include
    urlpatterns = [
        path('paypal/', include("paypal.standard.ipn.urls")),
  5. Whenever an IPN is processed a signal will be sent with the result of the transaction.

    The IPN signals should be imported from paypal.standard.ipn.signals. They are:

    • valid_ipn_received

      This indicates a correct, non-duplicate IPN message from PayPal. The handler will receive a paypal.standard.ipn.models.PayPalIPN object as the sender. You must check:

      • the payment_status attribute,
      • the business attribute to make sure that the account receiving the payment is the expected one,
      • the amount and currency (see example below),
      • any other attributes relevant for your case
    • invalid_ipn_received

      This is sent when a transaction was flagged - because of a failed check with PayPal, for example, or a duplicate transaction ID. You should never act on these, but might want to be notified of a problem.

    Connect the signals to actions to perform the needed operations when a successful payment is received (as described in the Django Signals Documentation).

    In the past there were more specific signals, but they were named confusingly, and used inconsistently, and are now deprecated. (See v0.1.5 docs for details)

    Example code: yourproject/

    from paypal.standard.models import ST_PP_COMPLETED
    from paypal.standard.ipn.signals import valid_ipn_received
    def show_me_the_money(sender, **kwargs):
        ipn_obj = sender
        if ipn_obj.payment_status == ST_PP_COMPLETED:
            # WARNING !
            # Check that the receiver email is the same we previously
            # set on the `business` field. (The user could tamper with
            # that fields on the payment form before it goes to PayPal)
            if ipn_obj.receiver_email != "":
                # Not a valid payment
            # ALSO: for the same reason, you need to check the amount
            # received, `custom` etc. are all what you expect or what
            # is allowed.
            # Undertake some action depending upon `ipn_obj`.
            if ipn_obj.custom == "premium_plan":
                price = ...
                price = ...
            if ipn_obj.mc_gross == price and ipn_obj.mc_currency == 'USD':

    Remember to ensure that import the hooks file is imported i.e. that you are connecting the signals when your project initializes. The standard way to do this is to create an AppConfig class and add a ready() method, in which you can register your signal handlers or import a module that does this.

    See the IPN/PDT variables documentation for information about attributes on the IPN object that you can use.

  6. You will also need to implement the return and cancel_return views to handle someone returning from PayPal.

    Note that the return view may need @csrf_exempt applied to it, because PayPal may POST to it (depending on the value of the rm parameter and possibly other settings), so it should be a custom view that doesn’t need to handle POSTs otherwise.

    When using PayPal Standard with Subscriptions this is not necessary since PayPal will route the user back to your site via GET.

    For return, you need to cope with the possibility that the IPN has not yet been received and handled by the IPN listener you implemented (which can happen rarely), or that there was some kind of error with the IPN.


If you are attempting to test this in development, using the PayPal sandbox, and your machine is behind a firewall/router and therefore is not publicly accessible on the internet (this will be the case for most developer machines), PayPal will not be able to post back to your view. You will need to use a tool like to make your machine publicly accessible, and ensure that you are sending PayPal your public URL, not localhost, in the notify_url, return and cancel_return fields.

Simulator testing

The PayPal IPN simulator at has some unfortunate bugs:

  • it doesn’t send the encoding parameter. django-paypal deals with this using a guess.

  • the default ‘payment_date’ that is created for you is in the wrong format. You need to change it to something like:

    23:04:06 Feb 02, 2015 PDT