Skip to Content
MiddlewareRuby / Rails Middleware

Ruby / Rails Middleware

singleform_webhook verifies SingleForm webhook signatures in Ruby. Works with Rails, Sinatra, and any Rack-based framework.

Installation

Add to your Gemfile:

gem "singleform_webhook"
bundle install

Configuration

# config/initializers/singleform.rb (Rails) SingleformWebhook.configure do |config| config.secret = ENV["SINGLEFORM_SECRET"] # Required: sf_secret_... config.timestamp_tolerance = 300 # Optional: seconds (default: 300) end

Rails

class WebhooksController < ApplicationController include SingleformWebhook::ControllerHelpers # Skip CSRF for webhook endpoint skip_before_action :verify_authenticity_token before_action :verify_singleform_webhook! def create form_id = singleform_metadata.form_id data = params.permit(:email, :name, :phone) # Your business logic user = User.find_or_create_by(email: data[:email]) do |u| u.name = data[:name] u.phone = data[:phone] end singleform_success(submission_id: user.id.to_s, message: "Welcome!") rescue ActiveRecord::RecordInvalid => e singleform_validation_error( { email: "is already taken" }, message: "Please correct the highlighted fields" ) end end

Add the route:

# config/routes.rb post "/webhooks/singleform", to: "webhooks#create"

Sinatra

require "sinatra" require "singleform_webhook" SingleformWebhook.configure do |config| config.secret = ENV["SINGLEFORM_SECRET"] end # Use as Rack middleware use SingleformWebhook::Middleware, paths: ["/webhooks/singleform"] post "/webhooks/singleform" do metadata = env["singleform"] data = JSON.parse(request.body.read) content_type :json { success: true, data: { submission_id: "123" } }.to_json end

Rack Middleware

Works with any Rack-compatible application:

# config.ru require "singleform_webhook" SingleformWebhook.configure do |config| config.secret = ENV["SINGLEFORM_SECRET"] end use SingleformWebhook::Middleware, paths: ["/webhooks/singleform"] run MyApp

The middleware stores verified metadata in env["singleform"]:

metadata = env["singleform"] metadata.form_id # => "form_abc123" metadata.timestamp # => 1700000000 metadata.nonce # => "a1b2c3..." metadata.verified # => true

Manual Verification

verifier = SingleformWebhook::Verifier.new(secret: ENV["SINGLEFORM_SECRET"]) begin metadata = verifier.verify( form_id: request.headers["X-SingleForm-Form-Id"], timestamp: request.headers["X-SingleForm-Timestamp"], nonce: request.headers["X-SingleForm-Nonce"], signature: request.headers["X-SingleForm-Signature"] ) puts "Verified! Form: #{metadata.form_id}" rescue SingleformWebhook::Error => e puts "Verification failed: #{e.type} - #{e.message}" end

Response Helpers

When using ControllerHelpers in Rails, three response methods are available:

# Success response singleform_success(submission_id: "123", message: "Done!") # => { success: true, data: { submission_id: "123", message: "Done!" } } # Business logic error singleform_error("DUPLICATE_SUBMISSION", "This email is already registered") # => { success: false, error: { type: "DUPLICATE_SUBMISSION", message: "..." } } # Field validation errors singleform_validation_error({ email: "Invalid format", phone: "Required" }) # => { success: false, error: { type: "VALIDATION_FAILED", message: "...", fields: { ... } } }

Error Types

Error ClassTypeDescription
MissingHeadersErrorMISSING_HEADERSRequired headers not present
InvalidTimestampErrorINVALID_TIMESTAMPTimestamp is not a valid integer
TimestampExpiredErrorTIMESTAMP_EXPIREDRequest is too old (replay protection)
InvalidSignatureErrorINVALID_SIGNATURESignature comparison failed
SignatureMismatchErrorSIGNATURE_MISMATCHHMAC does not match

Configuration Options

OptionDefaultDescription
secretRequired. Your webhook secret from the SingleForm dashboard.
timestamp_tolerance300Maximum age of a request in seconds before it’s rejected.
debugfalseEnable debug logging for troubleshooting.