Rails Integration
This guide shows a full Rails integration using push delivery with signature verification and background job processing.
Controller
Section titled “Controller”class WebhooksController < ApplicationController skip_before_action :verify_authenticity_token
def create unless verify_signature head :unauthorized return end
payload = JSON.parse(request.raw_post) event_id = payload["event_id"]
# Idempotency check if ProcessedWebhook.exists?(event_id: event_id) head :ok return end
# Enqueue for async processing ProcessWebhookJob.perform_later(payload) head :ok end
private
def verify_signature timestamp = request.headers["X-Gateway-Timestamp"] signature = request.headers["X-Gateway-Signature"] body = request.raw_post
expected = OpenSSL::HMAC.hexdigest( "SHA256", ENV.fetch("GATEWAY_DELIVERY_SECRET"), "#{timestamp}.#{body}" )
ActiveSupport::SecurityUtils.secure_compare(signature.to_s, expected) endendRails.application.routes.draw do post "/webhooks/transyt", to: "webhooks#create"endBackground Job
Section titled “Background Job”class ProcessWebhookJob < ApplicationJob queue_as :webhooks retry_on StandardError, wait: :polynomially_longer, attempts: 5
def perform(payload) event_id = payload["event_id"] provider = payload["provider"] event_type = payload["event_type"]
ActiveRecord::Base.transaction do # Record that we've processed this event return if ProcessedWebhook.exists?(event_id: event_id) ProcessedWebhook.create!(event_id: event_id)
# Route to the appropriate handler case provider when "stripe" handle_stripe(event_type, payload["payload"]) when "mailgun" handle_mailgun(event_type, payload["payload"]) else Rails.logger.warn("Unhandled provider: #{provider}") end end end
private
def handle_stripe(event_type, data) case event_type when "charge.succeeded" # Process successful charge when "charge.failed" # Handle failed charge when "customer.subscription.deleted" # Handle subscription cancellation end end
def handle_mailgun(event_type, data) case event_type when "delivered" # Mark email as delivered when "failed" # Handle delivery failure end endendIdempotency Model
Section titled “Idempotency Model”class CreateProcessedWebhooks < ActiveRecord::Migration[7.0] def change create_table :processed_webhooks do |t| t.string :event_id, null: false, index: { unique: true } t.timestamps end endendDatabase Polling (Alternative)
Section titled “Database Polling (Alternative)”If you prefer polling instead of push delivery, you can query the integration_events table directly:
class WebhookPumpJob < ApplicationJob queue_as :default
def perform # Query unprocessed events events = IntegrationEvent .where(status: "received") .order(received_at: :asc) .limit(100)
events.each do |event| # Claim the event atomically updated = IntegrationEvent .where(id: event.id, status: "received") .update_all(status: "enqueued")
next unless updated > 0
ProcessWebhookJob.perform_later( "event_id" => event.id, "provider" => event.provider, "event_type" => event.event_type_raw, "payload" => event.payload_raw ) end endendSchedule the pump job to run every 5 seconds using a recurring job framework (e.g., solid_queue, sidekiq-cron).