Bryan Helmig

Co-founder of Zapier, speaker, musician and builder of things.

If you use this, make sure you are PCI compliant, otherwise explore stripe.js…

In case you haven’t heard, payment gateways, merchant accounts and all that jazz are now obsolete thanks to Stripe. Stripe offers a simple to set up payment service with an absolutely wonderful API. Instead of comparing and contrasting dozens of merchant accounts and struggling with arcane API’s, with Stripe you input your name, address, SSN, and bank account information (among a few other things) and… you’re done.

In addition to a sane API, they offer great libraries for PHP, Python and Ruby along with wonderful documentation. Kudos to the developer team at Stripe, they’ve really outdone themselves.

Enough praising Stripe, let’s learn how to integrate it with Django. This should only take 20 minutes or so, and you shouldn’t have a problem bending this method to your will.

Things we need:

  1. Some way to store the sale in our database.
  2. A form that will validate the card details.
  3. A template to display the form with proper, human readable errors.
  4. The Stripe Python library:
    sudo pip install --index-url https://code.stripe.com --upgrade stripe

Luckily, we need not start from scratch on everything, as there is a very useful snippet for a credit card form we can start with. With that in mind, let’s lay out our project. I’ve decided to create a sales app that will contain: a Sale model, a SalePaymentForm, and a single view for displaying and parsing the form. First up, the Sale model in mystore/sales/models.py:

from django.db import models
 
import settings
 
class Sale(models.Model):
    def __init__(self, *args, **kwargs):
        super(Sale, self).__init__(*args, **kwargs)
 
        # bring in stripe, and get the api key from settings.py
        import stripe
        stripe.api_key = settings.STRIPE_API_KEY
 
        self.stripe = stripe
 
    # store the stripe charge id for this sale
    charge_id = models.CharField(max_length=32)
 
    # you could also store other information about the sale
    # but I'll leave that to you!
 
    def charge(self, price_in_cents, number, exp_month, exp_year, cvc):
        """
        Takes a the price and credit card details: number, exp_month,
        exp_year, cvc.
 
        Returns a tuple: (Boolean, Class) where the boolean is if
        the charge was successful, and the class is response (or error)
        instance.
        """
 
        if self.charge_id: # don't let this be charged twice!
            return False, Exception(message="Already charged.")
 
        try:
            response = self.stripe.Charge.create(
                amount = price_in_cents,
                currency = "usd",
                card = {
                    "number" : number,
                    "exp_month" : exp_month,
                    "exp_year" : exp_year,
                    "cvc" : cvc,
 
                    #### it is recommended to include the address!
                    #"address_line1" : self.address1,
                    #"address_line2" : self.address2,
                    #"daddress_zip" : self.zip_code,
                    #"address_state" : self.state,
                },
                description='Thank you for your purchase!')
 
            self.charge_id = response.id
 
        except self.stripe.CardError, ce:
            # charge failed
            return False, ce
 
        return True, response

A couple things to point out before moving on. One, we initialize the Stripe API in the __init__ method and use the API key you should set in settings.py as STRIPE_API_KEY = “somelongstripefromstripe”. Two, we wrap the actual charge API call in a try/except block so we can catch any errors Stripe might throw. Notice that we return that error as it is important to show the real, human readable error to the end user. You’ll see how in a second. Three, we make sure to set the charge_id on the mode, but we’ll need to remember to call the save() method.

Next, we need a form to handle the user’s data. Luckily, that snippet from before will come in handy, as the only thing we need to do is switch out the payment implementation. So, without further ado, here is the SalePaymentForm in mystore/sales/forms.py:

from datetime import date, datetime
from calendar import monthrange
 
from django import forms
 
from sales.models import Sale
 
class CreditCardField(forms.IntegerField):
    def clean(self, value):
        """Check if given CC number is valid and one of the
           card types we accept"""
        if value and (len(value) < 13 or len(value) > 16):
            raise forms.ValidationError("Please enter in a valid "+\
                "credit card number.")
        return super(CreditCardField, self).clean(value)
 
class CCExpWidget(forms.MultiWidget):
    """ Widget containing two select boxes for selecting the month and year"""
    def decompress(self, value):
        return [value.month, value.year] if value else [None, None]
 
    def format_output(self, rendered_widgets):
        html = u' / '.join(rendered_widgets)
        return u'<span style="white-space: nowrap;">%s</span>' % html
 
class CCExpField(forms.MultiValueField):
    EXP_MONTH = [(x, x) for x in xrange(1, 13)]
    EXP_YEAR = [(x, x) for x in xrange(date.today().year,
                                       date.today().year + 15)]
    default_error_messages = {
        'invalid_month': u'Enter a valid month.',
        'invalid_year': u'Enter a valid year.',
    }
 
    def __init__(self, *args, **kwargs):
        errors = self.default_error_messages.copy()
        if 'error_messages' in kwargs:
            errors.update(kwargs['error_messages'])
        fields = (
            forms.ChoiceField(choices=self.EXP_MONTH,
                error_messages={'invalid': errors['invalid_month']}),
            forms.ChoiceField(choices=self.EXP_YEAR,
                error_messages={'invalid': errors['invalid_year']}),
        )
        super(CCExpField, self).__init__(fields, *args, **kwargs)
        self.widget = CCExpWidget(widgets =
            [fields[0].widget, fields[1].widget])
 
    def clean(self, value):
        exp = super(CCExpField, self).clean(value)
        if date.today() &gt; exp:
            raise forms.ValidationError(
            "The expiration date you entered is in the past.")
        return exp
 
    def compress(self, data_list):
        if data_list:
            if data_list[1] in forms.fields.EMPTY_VALUES:
                error = self.error_messages['invalid_year']
                raise forms.ValidationError(error)
            if data_list[0] in forms.fields.EMPTY_VALUES:
                error = self.error_messages['invalid_month']
                raise forms.ValidationError(error)
            year = int(data_list[1])
            month = int(data_list[0])
            # find last day of the month
            day = monthrange(year, month)[1]
            return date(year, month, day)
        return None
 
class SalePaymentForm(forms.Form):
    number = CreditCardField(required=True, label="Card Number")
    expiration = CCExpField(required=True, label="Expiration")
    cvc = forms.IntegerField(required=True, label="CCV Number",
        max_value=9999, widget=forms.TextInput(attrs={'size': '4'}))
 
    def clean(self):
        """
        The clean method will effectively charge the card and create a new
        Sale instance. If it fails, it simply raises the error given from
        Stripe's library as a standard ValidationError for proper feedback.
        """
        cleaned = super(SalePaymentForm, self).clean()
 
        if not self.errors:
            number = self.cleaned_data["number"]
            exp_month = self.cleaned_data["expiration"].month
            exp_year = self.cleaned_data["expiration"].year
            cvc = self.cleaned_data["cvc"]
 
            sale = Sale()
 
            # let's charge $10.00 for this particular item
            success, instance = sale.charge(1000, number, exp_month,
                                                exp_year, cvc)
 
            if not success:
                raise forms.ValidationError("Error: %s" % instance.message)
            else:
                instance.save()
                # we were successful! do whatever you will here...
                # perhaps you'd like to send an email...
                pass
 
        return cleaned

I’ve removed a few of the bits from the snippet, mainly to simplify things, as we don’t need to filter out Discover cards or the like. The primary thing to notice is how if the charge isn’t successful, we raise a ValidationError and pass in the message from Stripe’s exception. This will allow us to display it as a normal error on the form. But before we we move on to the view, notice how we initialize a blank Sale? The charge() method doesn’t require a saved model instance, but we make sure to save it if it is successful. It might be more appropriate to make this a ModelForm, but for now, it works.

Let’s take a look at mystore/sales/urls.py:

from django.conf.urls.defaults import *
from sales import views
 
urlpatterns = patterns('',
    url(r'^charge/$', views.charge, name="charge"),
)

Nothing fancy here. Let’s take a look at and mystore/sales/views.py:

from django.shortcuts import render_to_response
from django.http import HttpResponse
from django.template import RequestContext
 
from sales.models import Sale
from sales.forms import SalePaymentForm
 
def charge(request):
    if request.method == "POST":
        form = SalePaymentForm(request.POST)
 
        if form.is_valid(): # charges the card
            return HttpResponse("Success! We've charged your card!")
    else:
        form = SalePaymentForm()
 
    return render_to_response("sales/charge.html",
                        RequestContext( request, {'form': form} ) )

Once again, nothing very fancy here either. Let’s take a look at templates/sales/charge.html:

<html>
<head>
  <title>Stripe Example</title>
</head>
<body>
 
<div class="wrapper">
 
  {% for key, value in form.errors.items %}
      <p>{{ value }}</p>
  {% endfor %}
 
  <form action="" method="post">{% csrf_token %}
 
    {% for field in form %}
      <div class="field-wrapper">
 
        <div class="field-label">
          {{ field.label_tag }}:
        </div>
 
        <div class="field-field">
          {{ field }}
          {{ field.errors }}
        </div>
 
      </div>
    {% endfor %}
 
    <br>
    <input type="submit" value="Charge Me!" />
  </form>
 
</div>
 
</body>
</html>

This should come as no surprise to you, but there isn’t much going on here. Probably the only slightly odd thing is the for loop over the form.errors dictionary, as this is where we will grab the ValidationError placed on the form itself as we caught it during the charge phase.

Besides for setting up the standard Django stuff like the database and admin bits (mystore/sales/admin.py), that pretty much covers it.

This is a method similar to what we’re using on our new product Zapier, so go grab the code off of GitHub!


Posted October 11, 2011 @ 12:14 am under Work.

As my next miniature project will be a crossword puzzle maker (note: domain has been sold to a nice fellow who is maintaining it) for teachers that will make random generation of crossword puzzles and word search puzzles, I thought I’d share the code I developed to create these puzzles on the fly. While I […]

Read more...


Posted April 10, 2010 @ 4:18 pm under Boring Stuff, Work.

Heads up, BitBuffet (the recommended service in this post) is not longer around. However, with a little elbow grease you can use my new startup Zapier to sell files online. There are quite a few services out there that provide a mechanism for digital downloads, most of them are cart based or even store based […]

Read more...


Posted February 15, 2010 @ 5:48 pm under Work.

In case you need a refresher, check out the tune here. While some are a stretch, a few are really quite relevant. 1. Never let no one know how much dough you hold. Keep your finances (good or bad) to yourself. Don’t make the mistake of bragging about how well or mentioning how badly you’re […]

Read more...


Posted February 5, 2010 @ 5:22 pm under Interesting.

Finally, after months of tweaking and building, I’ve launched Rankiac.com, a super charged automatic Google rank checker. It’s a dandy little SEO tool that doesn’t do a whole heck of a lot, but what it does, it does well. At the moment, it (1) tracks rankings in Google, (2) watches your important links and (3) […]

Read more...


Posted December 17, 2009 @ 3:55 am under Work.

The first consideration is: is the level of spending tied to the overall strategy? Given that there are uncertain returns for IT investments, the spending should be considered like any other business investment and prudence should be exercised just the same. While industry bookmarks can be an exceptional indicator, they should not be the targeted […]

Read more...


Posted November 4, 2009 @ 6:18 pm under Boring Stuff.

I love Django, and I love Django Snippets, but I’ve noticed some snippets are out of date, most notably for me, Django snippet 1095 or Django Encryption. Unfortunately, some folks are hitting a few snags on TypeError: “Non-hexadecimal digit found”. Luckily, it seems that Django-Fields have solved this problem for us! Here is my (their) […]

Read more...


Posted October 16, 2009 @ 12:56 am under Boring Stuff.

I never thought I’d cross into the dark side. Using AutoTune? On jazz?! What?!? Oh, I know how wrong it is. The “Trane” would roll over in his grave. Wes would be shocked. Miles would not approve. But, I don’t think those guys would listen to our records anyways (even if they were still alive). […]

Read more...


Posted August 13, 2009 @ 12:06 am under Interesting.

I run a few websites (lets just say over a dozen) so I generally spend a lot of my time optimizing and tweaking these sites. My first site, a free guitar lesson resource, survives solely off of Adsense. I like Adsense, its easy to use, is extremely popular, and there are is no shortage of […]

Read more...


Posted July 1, 2009 @ 4:27 pm under Work.