Easy payments with Stripe

Teamwork Engineering
Teamwork Engine Room

Why Teamwork chose Stripe.

Over the years we have tried and tested many payment providers; some good and some not so good. As Teamwork evolved we realized that we needed something that would work well for us and our 1 million users around the world. Stripe was an obvious choice as it provides great documentation, a simple dashboard, a very intuitive API and a huge array of webhook events which mean you can react to pretty much anything that occurs in relation to a payment.

On top of that, Stripe has an excellent client side library which takes a lot of the pain out of handling sensitive credit card information. I’m going to give a brief overview of the advantages Stripe offers and then discuss a few issues we faced when switching over to Stripe, and how we addressed them.

Stripe.js

First off, Stripe has a really useful JavaScript library, Stripe.js, which you can use to check the validity of credit card information before it’s even submitted to the server. You can check the credit card type, credit card number and CVV code using this, and alert the user of any inconsistencies before they submit their payment form. Here’s a simple example which uses functionality provided by the Stripe.card object to perform some checks:

function isCardValid() {
var cardErrors = '',
cardType = Stripe.card.cardType(
$('#creditCardNum').val()
),
cardRegexp = new RegExp(cardType, "i"),
acceptedCards = 'Visa, MasterCard, American Express';
// If the card is not in our list of accepted cards
if (!acceptedCards.match(cardRegexp)) {
if (cardType !== 'Unknown') {
var cardTypeString = "Sorry we do not accept " + cardType;
} else {
var cardTypeString = "Sorry we did not recognize your card.";
}
cardTypeString += "We only accept the following cards:" + acceptedCards;alert(cardTypeString);
return false;
}
// card number is invalid
if (!Stripe.card.validateCardNumber($('#creditCard').val())) {
if (!$('#creditCardNum').val()) {
cardErrors += "Please provide your credit card number.";
} else {
cardErrors += "Your credit card number looks incorrect.";
}
}
// ... more validation here, CVV check, expiration date check...if (cardErrors != '') {
alert("Please correct these problems and try again: " + cardErrors);
return false; // card is invalid
}
return true; // card is valid - POST form data to Stripe
}

While this is great as a utility library in itself, the real beauty of Stripe.js is its ability to encrypt sensitive payment data, saving us a lot of headaches. Using Stripe.js, you can securely submit the payment information to Stripe's servers, where it is encrypted and returned in a new safe-to-use object. Since this is all done via AJAX it means that the customer's credit card information has never touched your servers, so all you have to do is serve up your page over a https connection and you are automatically fully PCI compliant. Let's take a look:

Stripe.card.createToken({
number: $('#creditCardNum').val(),
exp_month: $('#expirationMonth').val(),
exp_year: $('#expirationYear').val(),
cvc: $('#securityCode').val(),
name: $('#userName').val()
}, callbackFn);

This will return a token from Stripe's server which represents the credit card data. We can now pass the ID of the token to our callbackFn to start creating payments. A typical callback function here would make an AJAX POST to Stripe's API to create a new customer, passing our token ID as the card parameter. Boom, we've just created a new customer, complete with payment info. Stripe will return a unique ID for this customer which we can store in our database and use to access this customer's account in future. For example, the next step would be an API POST to customers/:id, passing an ID as the plan parameter and we have just signed our new customer up to a recurring subscription.

Webhooks

So what happens at the end of the first billing period of our recurring subscription? Since Stripe automatically manages the recurring payments for us, we'll need to be kept up to speed on how everything is going. Luckily they have a vast list of webhooks to keep us up to date on any event that's of interest to us. Here are some of the more important webhooks that we're interested in and how we handle them:

  • invoice.payment_succeeded - We just got paid, let's go get some beers! Oh and we should probably issue an email receipt too...
  • invoice.payment_failed - Payment failed. This can happen for a number of reasons, e.g. the card has expired or the card has insufficient funds. Using Stripe's online dashboard, you can configure how this event is handled. The default is to re-attempt the payment 3 times, then cancel the subscription if still unsuccessful. It's also a good idea to send the customer an email on the first failed attempt.
  • invoice.created - This can be uselful for creating custom invoice items before the invoice is closed. More on this later.
  • customer.subscription.created,customer.subscription.updated - These events are fired when a customer switches to a new subscription. In this case, we need to update their details in our database to reflect their new pay plan.
  • customer.subscription.deleted - This can occur from an explicit action taken by a customer or as a result of too many failed payments. We will need to cancel the user's subscription within our app and send ourselves a cancellation email.

Testing

Even though we've got every webhook we could want, developing the code which handles them can sometimes become quite tedious. Since Stripe needs to send its webhooks to a live URL, we need to deploy our development code to a server in order to test it. If you've got a tricky little bug to fix, this can mean repeated deployment of tiny code changes just to track it down. One tool which can really simplify this workflow is ngrok. ngrok basiclly lets you expose a locally running web service to the internet. It does this by creating a tunnel from the public internet, e.g. subdomain.ngrok.com, to a port on your local machine. Here's a quick example using port 80:

ngrok 80

Yep, it's that easy. ngrok will then generate a subdomain which will let you access your local machine, saving you hours of pain:

http://3a4bfceb.ngrok.com -> 127.0.0.1:80

So you're probably thinking all this Stripe stuff is super awesome by now, and it is - but it does have some small limitations as well...

Handling Taxes

Stripe does not have an in-built functionality for dealing with taxes. I believe this is intentional in order to keep the product lean, but it does add a bit more complication to our workflow in certain cases. In our case, we need to charge some of our customers VAT, while others are exempt. We achieve this by creating a custom VAT invoice item and adding it to the customer's current invoice. Let's take a look at the steps involved:

  • We need to listen for an invoice.created event from Stripe's webhooks. Stripe waits a few hours between creating an invoice and charging it, so you have time to attach any extra invoice items to the invoice before it closes. We can use this timeframe to look up a customer and by their Stripe customer reference and check if they need to pay VAT on their account.
  • We calculate the VAT payable as a percentage of the total amount due on the invoice, then make an API POST to create a new invoice item for that amount. It’s important to specifically attach it to the currently open invoice (using the invoice param), otherwise it will not be applied until the next payment.

This all works nicely, but there are still a few edge cases we need to deal with:

New Customers

For example, when creating a customer for the first time, their invoice is immediately created and then closed. This gives us no time to add our VAT invoice item. In this case we need to create the VAT item for this customer and then make another API call assigning the customer to their pay plan. Simple...

Well, not always. In some rare occasions, a new customer can be successfully created with a valid credit card, but the credit card charge may be declined by their bank. If they decide to try again with a different card (or even a different pay plan), we could end up creating a second VAT invoice item for this customer. So how can we avoid this?

  • We log a “VAT pending” entry in our database when we create the VAT invoice item. This database entry will store a reference to the unique Stripe ID for the invoice item.
  • If the invoice payment is successful, we simply delete the database entry.
  • If the invoice payment fails and the customer tries again later, we check our database for any “VAT pending” entries for this customer. If we find an entry, we make an API call to delete the referenced invoice item, and then create a new invoice item for the correct amount.
  • Rinse. Repeat.

None of this is terribly difficult using Stripe's user friendly API but at the same time, it would be even better if we could just pass taxRate: 23 to a subscription API call.

Conclusion

So despite a couple of extra features we'd like to see, we're really pleased with our switch to Stripe. Even it's shortcomings can be overcome using its comprehensive selection of webhooks and and it's simple to use API. Want to try out the new payment system for yourself? Sign up at www.teamwork.com.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

No responses yet

Write a response