Handling Stripe Webhooks with Rails

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:

  private

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

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

    begin
      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
      return
    rescue Stripe::SignatureVerificationError => e
      # Invalid signature
      render json: { error: { message: e.message, extra: "Sig verification failed" }}, status: :bad_request
      return
    end

    # 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
    else
      puts "Unhandled event type: #{event.type}"
    end

    render json: { message: :success }
  end

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:

stripe:
  public_key: pk_test_456ghi
  private_key: sk_test_xyz789
  signing_secret:
  - 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:

bin/dev

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.