A fish trap for webhooks
Fuik (Dutch for fish trap) is a Rails engine that catches and stores webhooks from any provider. View all events in the admin interface, then create event classes to add your business logic.
Sponsored By Rails Designer
# Install
bundle add fuik
bin/rails generate fuik:install
bin/rails db:migrate
# Point your webhook to
POST https://yourdomain.com/webhooks/stripeThat's it. Webhooks are captured and visible at /webhooks.
Add to your Gemfile:
gem "fuik"Then run:
bundle install
bin/rails generate fuik:install
bin/rails db:migrateThe engine mounts at /webhooks automatically.
Visit /webhooks to see all received webhooks. Click any event to view all the payload details and copy the payload or download as JSON.
/webhooks path is by default not protected. Easiest is to set Fuik::Engine.config.events_controller_parent to a controller that requires authentication.
- Copy payload as JSON: click a button, payload is in your clipboard
- Download payload as JSON file: keep it for testing, debugging or throw it at your LLM agent, bot or colleague
- Add
.jsonto any URL: get the raw payload without the UI GET /events.json: index as JSON with filter support- Click any key to get the Ruby accessor path: click any key and get, e.g.
payload["line_items"][0]["product_id"](say what? 🤯)
Generate classes for events you want to process:
bin/rails generate fuik:provider stripe checkout_session_completedThis creates:
app/webhooks/stripe/base.rbapp/webhooks/stripe/checkout_session_completed.rb
Each class is a thin wrapper around your business logic:
module Stripe
class CheckoutSessionCompleted < Base
def process!
User.find_by(id: payload.client_reference_id).tap do |user|
user.activate_subscription!
user.send_welcome_email
# etc.
end
@webhook_event.processed!
end
end
endThe payload method supports dot notation and standard hash methods:
# Dot notation
payload.client_reference_id
payload.customer.email
# Hash syntax (strings/symbols)
payload["client_reference_id"]
payload[:customer_id]Implement Base.verify! to enable signature verification:
module Stripe
class Base < Fuik::Event
def self.verify!(request)
secret = Rails.application.credentials.dig(:stripe, :signing_secret)
signature = request.headers["Stripe-Signature"]
Stripe::Webhook.construct_event(
request.raw_post,
signature,
secret
)
rescue Stripe::SignatureVerificationError => error
raise Fuik::InvalidSignature, error.message
end
end
endIf Provider::Base.verify! exists, Fuik calls it automatically. Invalid signatures return 401 without storing the webhook.
Unit test your process! methods without a database using the built-in test helper:
# test/test_helper.rb
require "fuik/test_helpers"
class ActiveSupport::TestCase
include Fuik::TestHelpers
end# test/webhooks/stripe/checkout_session_completed_test.rb
module Stripe
class CheckoutSessionCompletedTest < ActiveSupport::TestCase
test "processes a completed checkout" do
event = build_webhook_event(
provider: "stripe",
event_type: "checkout.session.completed",
payload: { "client_reference_id" => "user_123" }
)
CheckoutSessionCompleted.new(event).process!
assert_equal "processed", event.status
end
test "fails when customer is not found" do
event = build_webhook_event(
provider: "stripe",
event_type: "checkout.session.completed",
payload: {}
)
CheckoutSessionCompleted.new(event).process!
assert_equal "failed", event.status
assert_equal "Customer not found", event.error
end
end
endYour process! calls @webhook_event.processed! on success and @webhook_event.failed!(error) on failure:
module Stripe
class CheckoutSessionCompleted < Base
def process!
user = User.find_by(id: payload.client_reference_id)
if user.present?
user.activate_subscription!
@webhook_event.processed!
else
@webhook_event.failed!("Customer not found")
end
end
end
endBy default:
- Development/test: all providers are allowed
- Production/staging: only providers in
app/webhooks/are allowed
Configure with Fuik::Engine.config.providers_allowed:
# Allow all (including production)
Fuik::Engine.config.providers_allowed = :all
# Explicit allowlist (overrides directory scan)
Fuik::Engine.config.providers_allowed = %w[stripe github shopify]Unknown providers return 404 Not Found.
Fuik includes ready-to-use templates for common providers.
Fuik automatically extracts event types and IDs from common locations:
Event Type:
- provider config (if exists);
- common headers (
X-Github-Event,X-Event-Type, etc.); - payload (
type,event,event_type); - falls back to
"unknown".
Event ID:
- provider config (if exists);
- common headers (
X-GitHub-Delivery,X-Event-Id, etc.); - payload (
id). - falls back to MD5 hash of request body.
Create app/webhooks/provider_name/config.yml:
event_type:
source: header
key: X-Custom-Event
event_id:
source: payload
key: custom_idThe options for event_type's source are:
- header
- payload
- static; for cases when no event type is present in header or payload
Have a provider template others could use? Add it to lib/generators/fuik/provider/templates/your_provider/ and submit a PR!
Include:
base.rb.ttwith signature verification (if applicable);- event class templates with helpful TODO comments.
This project uses Standard for formatting Ruby code. Please make sure to run rake before submitting pull requests.
The gem is available as open source under the terms of the MIT License.

