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 installConfiguration
# 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)
endRails
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
endAdd 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
endRack 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 MyAppThe middleware stores verified metadata in env["singleform"]:
metadata = env["singleform"]
metadata.form_id # => "form_abc123"
metadata.timestamp # => 1700000000
metadata.nonce # => "a1b2c3..."
metadata.verified # => trueManual 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}"
endResponse 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 Class | Type | Description |
|---|---|---|
MissingHeadersError | MISSING_HEADERS | Required headers not present |
InvalidTimestampError | INVALID_TIMESTAMP | Timestamp is not a valid integer |
TimestampExpiredError | TIMESTAMP_EXPIRED | Request is too old (replay protection) |
InvalidSignatureError | INVALID_SIGNATURE | Signature comparison failed |
SignatureMismatchError | SIGNATURE_MISMATCH | HMAC does not match |
Configuration Options
| Option | Default | Description |
|---|---|---|
secret | — | Required. Your webhook secret from the SingleForm dashboard. |
timestamp_tolerance | 300 | Maximum age of a request in seconds before it’s rejected. |
debug | false | Enable debug logging for troubleshooting. |