ruby 29 lines · 1 tab

Webhook signature verification

Alex Kumar Jan 2026
1 tab
module Webhooks
  class StripeController < ApplicationController
    skip_before_action :verify_authenticity_token

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

      begin
        event = Stripe::Webhook.construct_event(
          payload, sig_header, Rails.application.credentials.stripe[:webhook_secret]
        )
      rescue Stripe::SignatureVerificationError => e
        Rails.logger.warn("Webhook signature verification failed: #{e.message}")
        return render json: { error: 'INVALID_SIGNATURE' }, status: :unauthorized
      end

      # Process the verified event
      case event.type
      when 'payment_intent.succeeded'
        ProcessPaymentSuccessWorker.perform_async(event.data.object.id)
      when 'customer.subscription.deleted'
        ProcessSubscriptionCancelledWorker.perform_async(event.data.object.id)
      end

      render json: { received: true }, status: :ok
    end
  end
end
1 file · ruby Explain with highlit

When receiving webhooks from external services, signature verification ensures the payload comes from the claimed sender and hasn't been tampered with. Services like Stripe and GitHub include an HMAC signature in headers computed from the request body and a shared secret. I recompute the HMAC on the received body using the same secret and compare it to the provided signature using constant-time comparison to prevent timing attacks. The raw request body must be used for signature verification, not the parsed version, which is why I sometimes need to read request.body.read before Rails parses it. Failed verification should return 401 immediately without processing the payload to avoid acting on forged requests.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
ruby
payload = {
  sub: user.id,
  iss: 'https://auth.example.com',
  aud: 'codesnips-api',
  exp: 15.minutes.from_now.to_i,
  iat: Time.now.to_i,

JWT issuance and verification without common footguns

jwt authentication api
by Kai Nakamura 2 tabs
ruby
timestamp = request.headers.fetch('X-Signature-Timestamp')
signature = request.headers.fetch('X-Signature')
payload = request.raw_post

data = "#{timestamp}.#{payload}"
expected = OpenSSL::HMAC.hexdigest('SHA256', ENV.fetch('WEBHOOK_SECRET'), data)

HMAC signed API requests for webhook and partner integrity

hmac api-signing webhooks
by Kai Nakamura 2 tabs
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs

Share this code

Here's the card — post it anywhere.

Webhook signature verification — share card
Link copied