Schedule Webhooks from Rails
Schedule delayed tasks and webhooks from Rails without Sidekiq or Redis. Uses Net::HTTP and HMAC-SHA256 — no gem required. Works alongside Sidekiq for the timing-and-delivery subset.
Last updated: March 24, 2026
There is no Ruby SDK for Posthook. The integration uses Net::HTTP and OpenSSL::HMAC — both part of Ruby’s standard library. No gem to install, no dependency to manage. Schedule a hook with a JSON POST, receive the delivery at a Rails controller, verify the signature with HMAC-SHA256.
If you use Sidekiq, you can keep it for in-process background jobs and use Posthook for work that is really just waiting for a timestamp — a reminder in 48 hours, an expiration check in 7 days. This guide shows both patterns: scheduling directly, and scheduling a timed delivery that enqueues a Sidekiq job when it fires.
Setup
Store your API key and signing key in Rails credentials:
rails credentials:edit
posthook_api_key: phk_your_api_key
posthook_signing_key: ph_sk_your_signing_key
Create a service object for scheduling:
# app/services/posthook_client.rb
require "net/http"
require "json"
class PosthookClient
BASE_URL = "https://api.posthook.io/v1"
def schedule(path:, data: {}, **timing)
uri = URI("#{BASE_URL}/hooks")
body = { path: path, data: data }.merge(timing)
request = Net::HTTP::Post.new(uri)
request["Content-Type"] = "application/json"
request["X-API-Key"] = Rails.application.credentials.posthook_api_key
request.body = body.to_json
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
JSON.parse(response.body)
end
end
Schedule a hook
When an order is placed, schedule a payment timeout check for 30 minutes later:
class OrdersController < ApplicationController
def create
@order = Order.create!(order_params)
PosthookClient.new.schedule(
path: "/webhooks/order-timeout",
postIn: "30m",
data: { order_id: @order.id }
)
render json: { order_id: @order.id }, status: :created
end
end
The hook is persisted by Posthook. It survives deploys, restarts, and scaling events.
For user-facing reminders, postAtLocal schedules in the user’s timezone and handles DST transitions automatically:
PosthookClient.new.schedule(
path: "/webhooks/trial-reminder",
postAtLocal: "2026-04-05T09:00:00",
timezone: "America/Chicago",
data: { user_id: user.id }
)
Receive and verify the delivery
The webhook controller verifies the HMAC-SHA256 signature, parses the payload, and decides whether to act. The signature verification is manual — about 15 lines of Ruby:
# app/controllers/webhooks_controller.rb
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def handle
body = request.raw_post
unless verify_posthook_signature(body, request.headers)
head :unauthorized
return
end
payload = JSON.parse(body)
data = payload["data"]
case params[:action_name]
when "order-timeout"
handle_order_timeout(data)
when "trial-reminder"
handle_trial_reminder(data)
end
head :ok
end
private
def verify_posthook_signature(body, headers)
signing_key = Rails.application.credentials.posthook_signing_key
timestamp = headers["Posthook-Timestamp"]
signatures = headers["Posthook-Signature"]
return false unless timestamp && signatures
# Reject if timestamp is more than 5 minutes old
return false if (Time.now.to_i - timestamp.to_i).abs > 300
# Compute expected signature
signed_payload = "#{timestamp}.#{body}"
expected = "v1," + OpenSSL::HMAC.hexdigest("SHA256", signing_key, signed_payload)
# Compare against each space-separated signature (supports key rotation)
signatures.split(" ").any? do |sig|
ActiveSupport::SecurityUtils.secure_compare(sig, expected)
end
end
def handle_order_timeout(data)
order = Order.find(data["order_id"])
return if order.status != "pending"
order.update!(status: "expired")
OrderMailer.payment_timeout(order).deliver_later
end
def handle_trial_reminder(data)
user = User.find(data["user_id"])
return if user.subscription_active?
ReminderMailer.trial_expiring(user).deliver_later
end
end
Add the route:
# config/routes.rb
post "/webhooks/:action_name", to: "webhooks#handle"
The state check in each handler makes them idempotent. Posthook guarantees at-least-once delivery — the handler may run more than once for the same hook. Since handle_order_timeout checks order.status before acting, duplicates resolve as no-ops.
Working alongside Sidekiq
Posthook handles the durable timing. Sidekiq handles in-process execution with full Rails context. The delivery arrives at your Rails endpoint and enqueues a Sidekiq job:
def handle_trial_reminder(data)
user = User.find(data["user_id"])
return if user.subscription_active?
# Posthook handled the timing — Sidekiq handles the work
SendTrialReminderWorker.perform_async(user.id)
end
# app/workers/send_trial_reminder_worker.rb
class SendTrialReminderWorker
include Sidekiq::Worker
def perform(user_id)
user = User.find(user_id)
return if user.subscription_active?
ReminderMailer.trial_expiring(user).deliver_now
user.update!(trial_reminder_sent_at: Time.current)
end
end
This keeps scheduled jobs out of Redis sorted sets where they would sit for days consuming memory and adding polling overhead. Posthook schedules the delivery for the right time, Sidekiq processes the work immediately when it arrives.
Local development
Use the Posthook CLI to forward deliveries to your local Rails server — no deploy, no public URL:
npx posthook listen --forward http://localhost:3000
The CLI connects via WebSocket and forwards each delivery as an HTTP POST to your local server with the same headers and payload as production. Your handler’s response determines the outcome — 200 means delivered, 202 starts an async hook flow, and anything else triggers a retry. Retries are handled by Posthook the same way they are in production, with the same backoff and attempt limits. The CLI requires Node.js, but your Rails app just receives standard HTTP requests.
Next steps
- Quickstart guide — full setup walkthrough
- Expiration and timeout checks — the complete pattern with DIY comparison
- Reminders and follow-ups — onboarding, trials, follow-up chains
- Sidekiq vs Posthook — when to use a queue and when to use managed scheduling
- Pricing — free plan includes 1,000 hooks/month
Frequently asked questions
Ready to get started?
Create your free account and start scheduling hooks in minutes. No credit card required.