DEV Community

Maxence Henneron
Maxence Henneron

Posted on • Edited on

Handling stripe webhooks with Ruby on Rails

When accepting payments on a Ruby on Rails app, if you want to be aware of every actions that happen on stripe, you will have to implement the stripe webhooks.

I found a nice way to handle them, which I'm going to share in this article.

I strongly discourage you to implement them on your own by adding a simple POST route to /webhooks, because:

  • You need to check stripe's signature before accepting them (someone may be trying to impersonate Stripe!)
  • There are gems available to simplify this task

In this tutorial, we're going to use a gem called stripe_event

Let's get started

Installing the dependencies

The first step is to add the gem in your Gemfile. If you're new to ruby, the file is located at the root of your app.

gem 'stripe_event'
Enter fullscreen mode Exit fullscreen mode

Then, in your routes.rb file, located in /config, add the following line:

mount StripeEvent::Engine, at: '/stripe-webhooks' #you can change this url
Enter fullscreen mode Exit fullscreen mode

This will create a POST route to handle all the webhooks. We're going to implement that in a few moments, after we setup everything in the stripe dashboard

Setting up the webhooks

First, if you want to try the webhooks on your development environment, you will need to use a tool like ngrok.

Then, in your Stripe dashboard, navigate to Developers -> Webhooks or click on this link.

Finally, switch to "test data" and add a new endpoint. In "URL to be called", add your ngrok URL followed by the route name you chose earlier.

Once you created your webhook, please note the signing secret for that webhook somewhwere.
Signing secret

Adding the credentials

We will now add the stripe credentials to the (relatively) new rails encrypted credentials

To edit the credentials, type the following command:

EDITOR=nano rails credentials:edit
Enter fullscreen mode Exit fullscreen mode

and write the following lines:

stripe:
  development:
    publishable_key: 'pk_test_'
    secret_key: 'sk_test_'
    signing_secret: 'whsec_'
  production:
    publishable_key: 'pk_live_'
    secret_key: 'sk_live_'
    signing_secret: 'whsec_'
Enter fullscreen mode Exit fullscreen mode

publishable_key and secret_key are your stripe keys, you can find them in your account settings, signing_secret is the secret we generated earlier.

Configuring stripe_events

Create a file called "stripe_events.rb" in config/initializers and paste the following code

Stripe.api_key = Rails.application.credentials.stripe[Rails.env.to_sym][:publishable_key]
StripeEvent.signing_secret = Rails.application.credentials.stripe[Rails.env.to_sym][:signing_secret]

StripeEvent.configure do |events|
  events.subscribe 'invoice.', Stripe::InvoiceEventHandler.new
end
Enter fullscreen mode Exit fullscreen mode

The first two lines are setting the correct tokens, depending on your current environment (development or production)

In the last three lines, we are telling stripe_event to subscribe to all the events starting with 'invoice.' and redirect them to a class called "InvoiceEventHandler"

The next step is to implement this class.

In your app folder, create a new folder called "services" and add a folder called "stripe" in it.

Then, add a file called invoice_event_handler.rb in the stripe folder we just created.

module Stripe
  class InvoiceEventHandler
    def call(event)
      begin
        method = "handle_" + event.type.tr('.', '_')
        self.send method, event
      rescue JSON::ParserError => e
        # handle the json parsing error here
        raise # re-raise the exception to return a 500 error to stripe
      rescue NoMethodError => e
        #code to run when handling an unknown event
      end
    end

    def handle_invoice_payment_failed(event)
    end

    def handle_invoice_payment_succeeded(event)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Here, I implemented two events: invoice.payment.failed and invoice.payment.succeeded. Using 'send', I'm forwarding the event to the correct method. (Credit: @aradilopez)

That's it!

In my various projects, I'm redirecting all the useful notifications to a slack channel, so I'm always aware of what's happening.

Top comments (12)

Collapse
 
superails profile image
Yaroslav Shmarov

Receiving Stripe Webhooks 101

Webhooks = incoming POST requests from external services.

To receive incoming Stripe webhooks you do not need any gem.

First configure dashboard.stripe.com to receive wehooks (as described above)

Next, You need to add a route in routes.rb

   resources :webhooks, only: [:create]
Enter fullscreen mode Exit fullscreen mode

add a webhooks controller app/controllers/webhooks_controller.rb

class WebhooksController < ApplicationController
  skip_before_action :authenticate_user!
  skip_before_action :verify_authenticity_token

  def create
    payload = request.body.read
    sig_header = request.env['HTTP_STRIPE_SIGNATURE']
    event = nil

    begin
      event = Stripe::Webhook.construct_event(
        payload, sig_header, Rails.application.credentials.dig(:stripe, :webhook)
      )
    rescue JSON::ParserError => e
      status 400
      return
    rescue Stripe::SignatureVerificationError => e
      # Invalid signature
      puts "Signature error"
      p e
      return
    end

    # Handle the event
    case event.type
    when 'checkout.session.completed'
      session = event.data.object
      @user = User.find_by(stripe_customer_id: session.customer)
      @user.update(subscription_status: 'active')
    when 'customer.subscription.updated', 'customer.subscription.deleted'
      subscription = event.data.object
      @user = User.find_by(stripe_customer_id: subscription.customer)
      @user.update(
        subscription_status: subscription.status,
        plan: subscription.items.data[0].price.lookup_key,
      )
    end

    render json: { message: 'success' }
  end
end
Enter fullscreen mode Exit fullscreen mode

now you can receive webhooks when a subscription was updated or deleted, or when a checkout was completed.

git source

video source

Have a nice day!

Collapse
 
nklbigone profile image
nkl_alexis • Edited

thank you very much for your hard work

I am testing webhook locally I can receive 200 status on post request by my user table never being update I receive 500 status though

if I find user from table I can have him mean all response I can log them on when it comes to update table that's where the problem is

I am using this method you used

though I am running my app from docker is that could be the reason?

Collapse
 
tolgap profile image
Tolga Paksoy

This implementation will not work when your application is not eager loading (like in dev mode).

Your initializers are eager loaded. Anything in your "app" directory is auto loaded.

So you will most likely see errors in your webhook handlers that Stripe::InvoiceEventHandler cannot be reinitialized and is already in the constants tree.

You must move all event handlers to /lib if you want this. The issue then, is autoloading changes to these event handlers in dev mode.

Collapse
 
madeindjs profile image
Alexandre Rousseau

I did exactly the same thing without the StripeEvent gem. Here my post about my integration: rousseau-alexandre.fr/en/tutorial/...

Collapse
 
maxencehenneron profile image
Maxence Henneron

Thank you for sharing this. However, your implementation seem to lack signature verification, which is a security flow. That's the reason I suggested using the StripeEvent gem instead of implementing it manually :)

Collapse
 
merlin2049er profile image
Joe Guerra

can't seem to get a response from my app... i'm trying to trap the checkout_session_completed & charge_failed events from Stripe...
this is in my event handler... in addition to what's above

def handle_checkout_session_completed(event)
# your code goes here
render json: {message: 'Ok, great.'}
end

def handle_charge_failed(event)
# your code goes here
render json: {message: 'Not so good.'}
end

Collapse
 
andrewbrown profile image
Andrew Brown 🇨🇦

Sometimes I find my stripe webhooks fail so I have a cronjob setup to pull stripe once a day to to keep my Rails app in-sync with Stripe.

Collapse
 
happy199 profile image
Happy

Hello. I have a problems about rails stripe webhook. How can i configure stripe if i want to use many webhook endpoint with different signing_key.?

my config/initializers/stripe.rb is this :

StripeEvent.signing_secrets = [
Rails.application.secrets.stripe_ressource_payement_signing_secret,
Rails.application.secrets.stripe_course_payement_signing_secret,
]

and in my config/secret.yml i have :

development:
stripe_secret_key : 'sk_test_............'
stripe_publishable_key : 'pk_test_..........'
stripe_ressource_payement_signing_secret : 'whsec_.........'
stripe_course_payement_signing_secret : 'whsec_........'

But i don't know how to specify in my controller what endpoint to use

Collapse
 
birthdaycorp profile image
birthdaycorp

Thanks so much for posting this! You made it much easier to understand. One question though: if I want to perform an action on a record in my db when the webhook receives an "invoice_payment.succeeded" event, would I write that code in its handler? Presumably I'd pull the needed info from event.data.object?

Collapse
 
gmolini profile image
Guillermo Moliní

I get a undefined method `render' for #CustomerEventHandler:0x00000003ade7a8
whenever I try to handle an error. why are you able to render json?

Collapse
 
maxencehenneron profile image
Maxence Henneron • Edited

The EventHandler is a pure ruby object, and therefore does not have any of the rails helpers. It is actually an error in my article.

If you want to raise an error when the webhook fails, you can use

rescue JSON::ParserError => e
   handle_error()
   raise  # re-raise the exception.
end

The response sent by your server will be a 500 and the webhook will fail

Collapse
 
stenpittet profile image
Sten

Well done, that's a piece that has been missing in our integration so thanks for sharing this.