StripeEvent is built on the ActiveSupport::Notifications API. Incoming webhook requests are authenticated by retrieving the event object from Stripe. Define subscribers to handle specific event types. Subscribers can be a block or an object that responds to #call
.
# Gemfile
gem 'stripe_event'
# config/routes.rb
mount StripeEvent::Engine, at: '/my-chosen-path' # provide a custom path
# config/initializers/stripe.rb
Stripe.api_key = ENV['STRIPE_SECRET_KEY'] # e.g. sk_live_1234
StripeEvent.configure do |events|
events.subscribe 'charge.failed' do |event|
# Define subscriber behavior based on the event object
event.class #=> Stripe::Event
event.type #=> "charge.failed"
event.data.object #=> #<Stripe::Charge:0x3fcb34c115f8>
end
events.all do |event|
# Handle all event types - logging, etc.
end
end
class CustomerCreated
def call(event)
# Event handling
end
end
class BillingEventLogger
def initialize(logger)
@logger = logger
end
def call(event)
@logger.info "BILLING:#{event.type}:#{event.id}"
end
end
StripeEvent.configure do |events|
events.all BillingEventLogger.new(Rails.logger)
events.subscribe 'customer.created', CustomerCreated.new
end
StripeEvent.subscribe 'customer.card.' do |event|
# Will be triggered for any customer.card.* events
end
StripeEvent automatically fetches events from Stripe to ensure they haven't been forged. However, that doesn't prevent an attacker who knows your endpoint name and an event's ID from forcing your server to process a legitimate event twice. If that event triggers some useful action, like generating a license key or enabling a delinquent account, you could end up giving something the attacker is supposed to pay for away for free.
To prevent this, StripeEvent supports using HTTP Basic authentication on your webhook endpoint. If only Stripe knows the basic authentication password, this ensures that the request really comes from Stripe. Here's what you do:
-
Arrange for a secret key to be available in your application's environment variables or
secrets.yml
file. You can generate a suitable secret with therake secret
command. (Remember, thesecrets.yml
file shouldn't contain production secrets directly; it should use ERB to include them.) -
Configure StripeEvent to require that secret be used as a basic authentication password, using code along the lines of these examples:
# STRIPE_WEBHOOK_SECRET environment variable StripeEvent.authentication_secret = ENV['STRIPE_WEBHOOK_SECRET'] # stripe_webhook_secret key in secrets.yml file StripeEvent.authentication_secret = Rails.application.secrets.stripe_webhook_secret
-
When you specify your webhook's URL in Stripe's settings, include the secret as a password in the URL, along with any username:
https://stripe:my-secret-key@myapplication.com/my-webhook-path
This is only truly secure if your webhook endpoint is accessed over SSL, which Stripe strongly recommends anyway.
If you have built an application that has multiple Stripe accounts--say, each of your customers has their own--you may want to define your own way of retrieving events from Stripe (e.g. perhaps you want to use the user_id parameter from the top level to detect the customer for the event, then grab their specific API key). You can do this:
StripeEvent.event_retriever = lambda do |params|
api_key = Account.find_by!(stripe_user_id: params[:user_id]).api_key
Stripe::Event.retrieve(params[:id], api_key)
end
class EventRetriever
def call(params)
api_key = retrieve_api_key(params[:user_id])
Stripe::Event.retrieve(params[:id], api_key)
end
def retrieve_api_key(stripe_user_id)
Account.find_by!(stripe_user_id: stripe_user_id).api_key
rescue ActiveRecord::RecordNotFound
# whoops something went wrong - error handling
end
end
StripeEvent.event_retriever = EventRetriever.new
If you'd like to ignore particular webhook events (perhaps to ignore test webhooks in production, or to ignore webhooks for a non-paying customer), you can do so by returning nil
in you custom event_retriever
. For example:
StripeEvent.event_retriever = lambda do |params|
return nil if Rails.env.production? && !params[:livemode]
Stripe::Event.retrieve(params[:id])
end
StripeEvent.event_retriever = lambda do |params|
account = Account.find_by!(stripe_user_id: params[:user_id])
return nil if account.delinquent?
Stripe::Event.retrieve(params[:id], account.api_key)
end
StripeEvent can be used outside of Rails applications as well. Here is a basic Sinatra implementation:
require 'json'
require 'sinatra'
require 'stripe_event'
Stripe.api_key = ENV['STRIPE_SECRET_KEY']
StripeEvent.subscribe 'charge.failed' do |event|
# Look ma, no Rails!
end
post '/_billing_events' do
data = JSON.parse(request.body.read, symbolize_names: true)
StripeEvent.instrument(data)
200
end
Handling webhooks is a critical piece of modern billing systems. Verifying the behavior of StripeEvent subscribers can be done fairly easily by stubbing out the HTTP request used to authenticate the webhook request. Tools like Webmock and VCR work well. RequestBin is great for collecting the payloads. For exploratory phases of development, UltraHook and other tools can forward webhook requests directly to localhost. You can check out test-hooks, an example Rails application to see how to test StripeEvent subscribers with RSpec request specs and Webmock. A quick look:
# spec/requests/billing_events_spec.rb
require 'spec_helper'
describe "Billing Events" do
def stub_event(fixture_id, status = 200)
stub_request(:get, "https://api.stripe.com/v1/events/#{fixture_id}").
to_return(status: status, body: File.read("spec/support/fixtures/#{fixture_id}.json"))
end
describe "customer.created" do
before do
stub_event 'evt_customer_created'
end
it "is successful" do
post '/_billing_events', id: 'evt_customer_created'
expect(response.code).to eq "200"
# Additional expectations...
end
end
end
This button sends an example event to your webhook urls, including an id
of evt_00000000000000
. To confirm that Stripe sent the webhook, StripeEvent attempts to retrieve the event details from Stripe using the given id
. In this case the event does not exist and StripeEvent responds with 401 Unauthorized
. Instead of using the 'Test Webhooks' button, trigger webhooks by using the Stripe API or Dashboard to create test payments, customers, etc.
Special thanks to all the contributors.
Semantic Versioning 2.0 as defined at http://semver.org.
MIT License. Copyright 2012-2015 Integrallis Software.