ben tedder : code things

Setting up a Stripe webhook in Rails

Setting up a Stripe webhook for your customer subscriptions in Ruby on Rails (version 4, but version 5 should prove little difference).

The first step is to install the stripe and stripe_event gems:

gem 'stripe'
gem 'stripe_event'

Once those are installed, you'll need to get your publishable key and secret key from the Stripe dashboard. I'll leave it to you to put those keys wherever you put them. I'm hosting on Heroku, which makes it easy to setup ENV variables. For the sake of getting it up and running, just hardcode them straight in if you need to for now.

Next step is to create a file at config/initializers/stripe.rb.

At the top of this file you'll want to setup your Stripe gem:

Rails.configuration.stripe = {
  publishable_key: ENV['STRIPE_PUBLISHABLE_KEY'],
  secret_key: ENV['STRIPE_SECRET_KEY']
}

Stripe.api_key = Rails.configuration.stripe[:secret_key]

If you're already using Stripe elsewhere in your app you may already have this setup. Once you do, let's carry on.

Next we'll want to configure the StripeEvent gem (in the same file):

StripeEvent.configure do |events|
  events.all do |event|
    # target specific events here
  end
end

This method will setup a listener on any event that gets passed through. We'll want to check the event type and do stuff with it.

For now, go to the Stripe dashboard and set your webhook to broadcast all events. Eventually you'll be able to trim those down to just the ones you need. Here are the ones I'm using (for subscriptions):

  • customer.subscription.updated
  • customer.source.updated
  • customer.subscription.deleted
  • customer.deleted

I'm going to run through exactly how I use each of these.

For starters, it's helpful to know what I'm storing for each user in MySQL. I started off just storing the Stripe ID of the user and a status, then I tried to store a bit more information and realized I just had to keep going to back to Stripe to ask for more. I've finally settled (for better or worse) on having the whole customer object stored in a JSON string in the field for the user. This means at any point in time I have access to which plan the user is on, if they have payment sources setup, and the status of their account. This lets me block usage of the app, alert them when their trial is up, etc.

So, let's get to each event:

customer.subscription.udated

Read the docs for when this even fires, but basically if a subscription has been updated I want to get that new customer object and just overwrite what I have in the DB (remember, this block of code goes in the commented section in the block of code mentioned above):

if event.type == 'customer.subscription.updated'
  subscription = event.data.object
  user = User.where('stripe_id LIKE ?', "%#{subscription['customer']}%").first
  if user
    customer = Stripe::Customer.retrieve(JSON.parse(user.stripe_id)['id'])
    user.stripe_id = customer.to_json
    user.save
  end
end

So because I'm storing the customer object in MySQL, I'm using my Active Record User model to find any user whose stripeid is LIKE the stripe id I've passed in. I'm confident in the uniqueness of the hash that Stripe sends me for the user ID. Plus it is prefixed with "cus" so I know it will only grab customer IDs.

Once I grab the first customer's row (and confirm there is indeed a customer with that ID) I take that ID and fetch it from Stripe again. Remember, the Stripe event.data.object is only sending me a subscription object. I don't want to have to parse the JSON I have stored, overwrite the user's subscription, etc. I want to get it straight from the source. Essentially I'm using this endpoint as just a receiver of a broadcast that "something has changed", and then I'm pulling the true data straight from the source.

customer.source.updated

This is going to look really familiar. I'm doing the exact same process when the source is updated: ie, going back to the well to get the full customer object:

if event.type == 'customer.source.updated'
  source = event.data.object
  user = User.where('stripe_id LIKE ?', "%#{source['customer']}%").first
  if user
    customer = Stripe::Customer.retrieve(JSON.parse(user.stripe_id)['id'])
    user.stripe_id = customer.to_json
    user.save
  end
end

customer.subscription.deleted

This one started out different. When a customer's subscription was deleted I just removed the whole field value that contained the customer object. This was a mistake, because if that user signed up for a different subscription then I couldn't use their old account, they ended up with 2 accounts. So I decided to just store the customer object with the new information that they had no subscriptions.

if event.type == 'customer.subscription.deleted'
  subscription = event.data.object
  user = User.where('stripe_id LIKE ?', "%#{subscription['customer']}%").first
  if user
    customer = Stripe::Customer.retrieve(JSON.parse(user.stripe_id)['id'])
    user.stripe_id = customer.to_json
    user.save
  end
end

customer.deleted

This one is slightly different. In the case that a customer is deleted from Stripe, then there is no harm in just deleting their Stripe info from MySQL:

if event.type == 'customer.deleted'
  customer = event.data.object
  user = User.where('stripe_id LIKE ?', "%#{customer['id']}%").first
  if user
    user.stripe_id = nil
    user.save
  end
end

Don't forget the route!

The last and final thing you need to do to get this working is setup the route. In config/routes.rb simply put this route wherever you need:

mount StripeEvent::Engine, at: '/stripe-event'

Deploy!

As you may have discovered, Stripe can't hit your Rails server at localhost:3000! So the only true way to test this is in some kind of deployed environment. That's the trickiest part of the whole thing. Because if you're working with some kind fo slow deploy process, every mistake you make takes a lot of back and forth with redeploying. I would highly recommend Heroku, as there is a free plan for testing out this kind of stuff.

Good luck!