Handle Paystack Webhooks in Ruby on Rails
To handle Paystack webhooks in Rails, add skip_forgery_protection to your webhook action, read request.body.read for the raw bytes, compute an HMAC SHA-512 hash with your secret key, compare it to the X-Paystack-Signature header, and process the event. Return head :ok.
Webhook Controller
# app/controllers/webhooks_controller.rb
class WebhooksController < ApplicationController
skip_forgery_protection only: :paystack
def paystack
signature = request.headers['X-Paystack-Signature']
return head :bad_request unless signature.present?
body = request.body.read
secret = ENV['PAYSTACK_SECRET_KEY']
computed_hash = OpenSSL::HMAC.hexdigest('sha512', secret, body)
unless ActiveSupport::SecurityUtils.secure_compare(computed_hash, signature)
return head :bad_request
end
event = JSON.parse(body)
ProcessPaystackWebhookJob.perform_later(event)
head :ok
end
end
ActiveJob for Processing
# app/jobs/process_paystack_webhook_job.rb
class ProcessPaystackWebhookJob < ApplicationJob
queue_as :default
def perform(event)
event_type = event['event']
data = event['data'] || {}
case event_type
when 'charge.success'
PaystackService.new.verify_and_fulfill(data['reference']) if data['reference']
when 'subscription.create'
handle_subscription_create(data)
when 'invoice.payment_failed'
handle_invoice_failed(data)
end
end
private
def handle_subscription_create(data)
# Create or update subscription record
end
def handle_invoice_failed(data)
# Update subscription status to past_due
end
end
Route
# config/routes.rb
post '/webhooks/paystack', to: 'webhooks#paystack'
Testing
# test/controllers/webhooks_controller_test.rb
test 'valid webhook returns 200' do
payload = { event: 'charge.success', data: { reference: 'test_123' } }.to_json
signature = OpenSSL::HMAC.hexdigest('sha512', ENV['PAYSTACK_SECRET_KEY'], payload)
post webhooks_paystack_url,
params: payload,
headers: { 'Content-Type' => 'application/json', 'X-Paystack-Signature' => signature }
assert_response :ok
end
Production Notes
- Use Sidekiq or another background job backend. Do not use inline jobs in production.
- HTTPS required. Your webhook URL must use HTTPS.
- Log events. Store every webhook in a database for debugging.
- Monitor. Check Paystack dashboard for failed deliveries.
Key Takeaways
- ✓Add skip_forgery_protection to your webhook controller action.
- ✓Use request.body.read for the raw bytes needed for HMAC verification.
- ✓Use OpenSSL::HMAC.hexdigest for SHA-512 hash computation.
- ✓Use ActiveSupport::SecurityUtils.secure_compare for constant-time comparison.
- ✓Return head :ok immediately. Process events in ActiveJob for reliability.
- ✓Make handlers idempotent. Paystack retries failed deliveries.
Frequently Asked Questions
- Why use skip_forgery_protection instead of protect_from_forgery?
- skip_forgery_protection is the Rails 7+ way to disable CSRF for specific actions. Paystack webhook requests do not include Rails CSRF tokens.
- Can I use request.raw_post instead of request.body.read?
- Yes. request.raw_post also returns the raw request body as a string. Both work for HMAC verification.
- Should I use ActiveJob or Sidekiq directly?
- ActiveJob is the Rails abstraction. Use it with Sidekiq as the backend adapter. This keeps your code framework-agnostic while getting Sidekiq performance.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon