How to use new Stripe Checkout with Ruby on Rails application

Illustration of a person looking confused at a broken cash register overflowing with paper snippets, implying a malfunction or unexpected event. Illustration of a person looking confused at a broken cash register overflowing with paper snippets, implying a malfunction or unexpected event.

Since day one mkdev uses Stripe for credit card processing. We were and still are very happy with our choice, as there are not so many easy-to-integrate, developer-friendly systems.

Initial version of mkdev used Stripe Checkout -- ready to use popup from Stripe. We would pass few variables and the rest was handled by a nicely designed widget from Stripe. As we grew, we required a bit more flexibility, especially around implementing our slider widget, which allows to choose for how many weeks you want to hire a mentor. At that point we threw out Stripe Checkout and instead went with implementing complete payment flow on our own. We would invoke Stripe API on our backend, Stripe.js on frontend and it all kind of worked nicely together for many years.

One thing we didn't like is that we still had to maintain quite a bit of code on our side to work with Stripe. We would workaround certain edge cases with credit card processing and we would issue and capture Stripe charges ourselves. Another disadvantage would be that when something like Strong Customer Authentication appears we would have to account for it ourselves.

The new Stripe Checkout and it's benefits

Luckily, in April 2019 Stripe released brand new Stripe Checkout. The major difference from old Stripe Checkout is that it's not a popup window directly on your site anymore. New Stripe Checkout is a Stripe-hosted nicely designed page which you redirect your customers to. It has few very nice features, including:

  1. Automatic support for more payment methods - different cards and Apply Pay from the start and more to come, like various EU payment flows;

  2. It's SCA-compliant. It's also compliant with many other things that we don't have to think about anymore. And if there will be a new regulation, we don't have to implement it -- Stripe is supposed to do that.

  3. Well designed page. New Stripe Checkout works across all devices, looks and feels nice to interact with and looks very legit. Feedback we got from some of our customers is that our credit card form doesn't look official enough. Stripe Checkout looks as fancy and official as it gets.

Switching from old Stripe Checkout to the new Stripe Checkout

As we had plans to upgrade Stripe API version we use (and we were using the one from 2014, oops), we decided to go a mile further and just switch our Ruby on Rails application to use this new Stripe Checkout.

Stripe had an excellent documentation for Ruby developers around implementing complete payment flow with old Checkout version. They didn't update it to match the new Checkout when we were integrating it.

The main trick with new Stripe Checkout is that payment flow is now asynchronous. Previously it looked like this:

  1. Open a profile of one of our mentors;

  2. Fill in your credit card details

  3. Hit the button

After the last step Stripe and us would process your payment and prepare your subscription. There were no redirects or nothing, everything happened step by step.

With new Stripe Checkout process looks like this:

  1. Open a profile of one of our mentors;

  2. Click on Pay button

  3. You are redirected to Stripe Checkout page

  4. Fill in your credit card (or Apply Pay) details

  5. If payment is succesful, you are redirected back to mkdev

The trick is how to handle what happens between step 4 and 5, because we need to store some payment data (like the amount you paid, the mentor you paid for etc) in our database and also create your subscription, invite you to im.mkdev.me, send a bunch of notifications and so on. We need to do what Stripe calls Purchase Fulfillment.

Recommended method to do so is via a Webhook. When your user submits the credit card form Stripe gives your backend 10 seconds to respond to the webhook, and only then it will redirect your customer to success_url. The final process looks like this:

  1. Open a profile of one of our mentors;

  2. Click on Pay button

  3. Your backend creates new Stripe::Checkout::Session object which you need to pass to frontend. You can also initiate this object on frontend, if you want.

  4. Stripe.js recieves Stripe Checkout Session ID and redirects you to Stripe Checkout page

  5. Fill in your credit card (or Apply Pay) details and submit the form

  6. Stripe processes the data and sends a webhook to your backend

7.1 If your backend responds with 200, Stripe will redirect your customer to success_url

7.2 If your backend doesn't respond, Stripe will redirect your customer to success_url anyways, but it will wait for 10 seconds first.

It is very critical for you to process this webhook correctly. It is very easy to end up in a situation, when your customer gave you money, but your backend code didn't fulfill the payment correctly, leading to your application being unaware of successful payment and your customer being pissed off. So what do you do?

Introducing Payment Sessions

The way we solved it at mkdev is by introducing some extra glue between our application and Stripe. We call it Payment Sessions. This is how it works:

Initiate Payment Session

When customer clicks Pay button we create a new Stripe::Checkout::Session object. But what we also do is we create our own special PaymentSession object, which is a regular ActiveRecord model. This model has multiple fields, like stripe_checkout_session_id, stripe_payment_intent_id and stripe_event_id. It is also a state machine (we use AASM, which initial state being created. Before we create Stripe Checkout Session we first create our own PaymentSession, id of which is then sent to Stripe as a metadata. And right after we create Stripe session we do:

user.payment_sessions.update_attributes(stripe_checkout_session_id: stripe_checkout_session.id,
                              stripe_payment_intent_id: stripe_checkout_session.payment_intent)

This way we can always quickly re-conciliate Stripe session with our database and our user.

Process Stripe webhook

After succesful payment, Stripe sends as a webhook, which we treat as a proof that money did leave users card and moved towards our bank. This is enough for us to move PaymentSession to the next state: paid:

payment_session = PaymentSession.find_by(stripe_checkout_session_id: event.data.object.id)
unless payment_session.stripe_event_id == event.id
   payment_session.update_attributes(stripe_event_id: event.id)
   payment_session.pay!
    FulfillMentorshipPaymentJob.perform_later(...)
end

Note that we make sure we don't process same event twice -- otherwise we would create a subscription too long, which would make our customer happy, but would also upset our mentor.

Fulfill the purchase

If you look close at the code, what we do next is we trigger our background job to fulfill the purchase. We do it in the background, because it is quite a complex process, implemented via multiple interactors. We don't want to risk this to fail and never retry, and we also don't want to risk that for some reason it takes longer than generous 10 seconds Stripe gave us to process a webhook.

At the very end of this job we move PaymentSession to its final state fulfilled. Now customer is all set to use the service she or he paid for.

User experience after Stripe Checkout

Because we can not be 100% sure that background job did finish in 10 seconds, we redirect user to a separate page with a loading indicator. On this page, we poll our backend to check that purchase was fulfilled and after it was, we redirect user to a nicely designed onboarding page:

The result of moving to the new Stripe Checkout

So far we are very happy with the new Stripe Checkout. It gives us a lot of peace of mind knowing, that our payment page is handled by Stripe, will be extended over time with many payment menthods and will always be complaint. We did our best to make sure that the new process is reliable and leads to best user experience from our side and we also introduced all the measures to make sure that even if something goes wrong, we will be the first to know about it and we will have all the data and means to resolve the problem. Another benefit is of course that we also know when someone initiated a new payment session and for some reason decided not to pay -- this makes our marketing team especially happy.

Hopefully this post helped you to make up your mind around using brand new Stripe Checkout and how to integrate it with your backend -- be it Ruby on Rails or something else.