How to add webhooks to Rails 5

October 01, 2018

When something important happens on an external service (think Stripe), their system can send a POST request to your app.1 This is called a webhook. Webhooks help automate operations without us having to do anything other than set them up. In this post, I’ll show you how to consume these webhooks with your Rails 5 application.

1. Create a POST route inside routes.rb.

post '/hooks/:integration_name' => 'webhooks#receive'

Let’s say you’re setting up Stripe. The code above would allow them to post to yoursite.com/hooks/stripe.

2. Generate a webhooks controller with a receive method2:

rails g controller Webhooks receive

3. Let’s work with the data we receive.

class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  def receive
    # Parse the data received as JSON
    data = JSON.parse(request.body.read)

    # Now you can use the data however you'd like.
    puts data['xxx']

    # Respond that we received the hook
    head :ok
  end
end

4. Technically, you can be done after Step 3. However, if you’re consuming multiple types of events, wouldn’t it be nice to have a separate action for each one? We can change the receive method to call a different function for each type of event:3

class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  def receive
    data = JSON.parse(request.body.read)
    method = "handle_" + data['type'].tr('.', '_')
    self.send method, data
    head :ok
  end

  def handle_charge_succeeded(data)
    # puts data['xxx']
  end

  def handle_charge_failed(data)
    # puts data['xxx']
  end

  def handle_customer_subscription_created(data)
    # puts data['xxx']
  end
end 
  1. This POST request can contain data about what just happened – i.e. “Customer RSwanson was successfully charged for their monthly subscription.” Your application could consume this data to update RSwanson’s account_standing to :active↩︎

  2. Alternatively, you could add a receive method to one of your existing controllers. If you do this, you’ll also need to point your route from Step 1 to the appropriate controller. ↩︎

  3. This is a pattern from Laravel Cashier and was introduced to me by Arandi López’s post↩︎