Handling Stripe Webhooks with Rails

Posted on

This is mostly for my own reference later so I can quickly copy and paste snippets.

First, I create a webhook controller:

rails g controller Webhooks

Then, configure the routes to accept POST requests

# config/routes.rb

resources :webhooks, only: [:create]

Then, I make sure to skip CSRF protection, which doesn't make sense for webhooks.

# app/controllers/webhooks_controller.rb

class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token

Next, I'll add a private method to fetch the webhook endpoint secret:


def endpoint_secret
(Rails.application.credentials.dig(:stripe, :signing_secret) || []).first

Then, I'll drop in this code which is a smiple getting started, but ultimately I often need to expand to using Jobs for processing.

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

event = Stripe::Webhook.construct_event(
payload, sig_header, endpoint_secret
rescue JSON::ParserError => e
# Invalid payload
render json: { error: { message: e.message }}, status: :bad_request
rescue Stripe::SignatureVerificationError => e
# Invalid signature
render json: { error: { message: e.message, extra: "Sig verification failed" }}, status: :bad_request

# Handle the event
case event.type
when 'payment_intent.succeeded'
payment_intent = event.data.object # contains a Stripe::PaymentIntent
puts 'PaymentIntent was successful!'
when 'payment_method.attached'
payment_method = event.data.object # contains a Stripe::PaymentMethod
puts 'PaymentMethod was attached to a Customer!'
# ... handle other event types
puts "Unhandled event type: #{event.type}"

render json: { message: :success }

To confirm the endpoint secret is set up correctly, edit the credentials:

EDITOR=vi rails credentials:edit

Confirm the yaml has something like this shape:

public_key: pk_test_456ghi
private_key: sk_test_xyz789
- whsec_abc123

I like to test and build webhooks with the Stripe CLI, so I'll print the secret to confirm it's the one that I'll use with the listen command:

stripe listen --print-secret

In this case, it printed whsec_fd03884b23637875a5de75b850eaff56272adb133ce67d53d8c55e6d8bc77046 and that will be the webhook signing secret used for the webhook endpoint created by the Stripe CLI automatically when I run the stripe listen command.

I've started using bin/dev to start my rails apps recently. It can be helpful to add a line to always start the webhook listener here too.

Here's what my Procfile.dev looks like:

web: bin/rails server -p 3000
js: yarn build --watch
css: yarn build:css --watch
stripe: stripe listen --forward-to localhost:3000/webhooks -c localhost:3000/webhooks
jobs: QUEUE=* rake resque:work

Now that the webhook is configured, I'll fire up the app:


Note that when the app starts, the output will also include the webhook signing secret, in case it wasn't printed earlier:

screenshot of terminal with output showing signing secret

At this point, we can test to see if the webhook endpoint is working using the Stripe CLI.

We should log out PaymentIntent was successful! when receiving a payment_intent.succeeded event.

With the trigger command in the Stripe CLI, we can cause that event to fire:

stripe trigger payment_intent.succeeded

I check the server log and confirm that I see that message printed and now I'm ready to move onto app specific event handling logic.