Creating and testing Stripe Webhooks using StripeEvent

Listening for invoice.payment_failed

Rainbow of Credit by Frankie Leon is licensed under the CC BY 2.0

We use Stripe for accepting payments for our subscriptions in the apps we build. There are a variety of events that can happen within Stripe, but today we’re looking at just one.

Let’s say that one year passes and it is time for the user to pay for their subscription. Stripe will automatically try to charge the card stored on Stripe’s server. If the card is invalid at that time, an event is created when the card fails, called invoice.payment_failed. Today, I am going to use Stripe webhooks to listen for that event. When I mentioned this was what I was planning to do, a colleague suggested using the StripeEvent gem.

Lets get this party started!

1. Add StripeEvent

In your Gemfile add:

gem "stripe_event"

Head over to the terminal and type:

$ bundle install

Now add this to your routes file:

mount StripeEvent::Engine, at: "/stripe-events"

Finally, restart local server.

2. Add the Endpoint in Stripe

Collective Idea - Stripe.gif

Head on over to Stripe and log in. In the upper right-hand corner you’ll see your account name and a dropdown menu. Click the dropdown menu and choose “Account Settings”. A new window will now pop up.

In that new window, click “Webhooks”.

Click “Add endpoint…”.

Enter the URL of the website you’re working with into the URL field, followed by /stripe-events.

Select “test” for the mode.

Now hit “Create endpoint”. Your current pop-up window you were working in will disappear.

Now that you’ve created your endpoint, you’ll want to click the button that says “Send test webhook…”.

Another pop-up will appear and you’ll hit “Send test webhook…” again.

If you receive a TLS response, the webhook is not making it to the app. I received this error when I had the URL wrong.

You will get a 403 error because there is no way in yet due to the firewall. We need to blow a hole in it for a moment.

In comes ngrok.

3. Blow the hole

Collective Idea - Blow the Hole.gif

Create a file config/initializers/stripe.rb.

Stripe.api_key = Rails.application.config.stripe.secret_key # set in secrets.yml

StripeEvent.configure do |events|
  events.subscribe "invoice.payment_failed" do |event|
    stripe_customer_id = user.event.data.object.customer
    user = User.find_by(stripe_id: stripe_customer_id)
    PaymentMailer.payment_failed(user).deliver_now if user
  end
end

Head over to the terminal to install ngrok.

$ brew cask install ngrok

Blow the hole using:

$ ngrok http 3000

Copy one of the Forwarding addresses up to the “->” http://0eb07e6e.ngrok.io

Now, head over to where you are running your local environment and replace the URL with the forwarding copied address.

Refresh the page. Now you can receive the call through the hole from Stripe.

Next, you’re going to go back over to Stripe in order to send another webhook.

Navigate back to the webhooks page. Next to the test URL, you’ll see the option to edit. Click “Edit”.

Now, replace the previous URL we had in there with the ngrok URL from the terminal followed by /stripe-events.

Click “Update endpoint”.

Click “Send test webhook”.

A pop-up window will appear where you will see the ngrok URL /stripe-events ready to send.

Click “Send test webhook”.

This time you should see “Test webhook sent successfully”.

We are in!

If you go over to your server you will see what was received.

4. Record what comes in the hole to use for testing

Collective Idea - Record.gif

Go to https://requestb.in/.

Click “Create a RequestBin”.

Scroll to the Ruby section.

Copy the first line you see there. Now you are going to head over to the URL you’re currently at in the browser bar, add a ? and paste the line you just copied. You will do that with the next two lines in the Ruby section. There should be a ? between each line you copy and paste into your browser bar.

Your URL should look similar to this this:

https://requestb.in/186cchs1?inspect?require 'open-uri'?result = open
('http://requestb.in/186cchs1')?result.lines { |f| f.each_line {|line| p line} }

Press Enter.

If properly configured, the word “OK” will appear in the upper left-hand corner.

Click the browser back button.

Collective Idea - Request Bin.gif

Now scroll up to the top of the page and copy the “Bin URL”.

Navigate back to Stripe to change out the test endpoint URL we’ve been using with the new request bin one you just copied. Now click “Update endpoint”.

We’re going to send a “Test webhook” again, but this time change the event to invoice.payment_failed (you can choose which ever event you are trying to receive).

Click “Send test webhook”. You should see “Test webhook sent successfully” if everything is working.

If you click the arrow next to “Test” you will see the familiar “OK” from RequestBin.

Go back over to the RequestBin browser tab, refresh the window, and we should see our capture!

Yep BEAUTIFUL!!!

Scroll down and copy RAW BODY contents.

Create a file spec/fixtures/payment_failed_spec.json.

Paste the RAW BODY contents:

{
  "created": 1326853478,
  "livemode": false,
  "id": "evt_00000000000000",
  "type": "invoice.payment_failed",
  "object": "event",
  "request": null,
  "pending_webhooks": 1,
  "api_version": "2016-07-06",
  "data": {
    "object": {
      "id": "in_00000000000000",
      "object": "invoice",
      "amount_due": 16900,
      "application_fee": null,
      "attempt_count": 1,
      "attempted": true,
      "charge": "ch_00000000000000",
      "closed": false,
      "currency": "usd",
      "customer": "cus_00000000000000",
      "date": 1478115204,
      "description": null,
      "discount": null,
      "ending_balance": 0,
      "forgiven": false,
      "lines": {
        "data": [
          {
            "id": "sub_00000000000000",
            "object": "line_item",
            "amount": 16900,
            "currency": "usd",
            "description": null,
            "discountable": true,
            "livemode": true,
            "metadata": {},
            "period": {
              "start": 1509651204,
              "end": 1541187204
            },
            "plan": {
              "id": "authentic_texts",
              "object": "plan",
              "amount": 16900,
              "created": 1476899728,
              "currency": "usd",
              "interval": "year",
              "interval_count": 1,
              "livemode": false,
              "metadata": {},
              "name": "Authentic Texts",
              "statement_descriptor": null,
              "trial_period_days": null
            },
            "proration": false,
            "quantity": 1,
            "subscription": null,
            "type": "subscription"
          }
        ],
        "total_count": 1,
        "object": "list",
        "url": "/v1/invoices/in_000000000000000000000000/lines"
      },
      "livemode": false,
      "metadata": {},
      "next_payment_attempt": null,
      "paid": false,
      "period_end": 1478115204,
      "period_start": 1478115204,
      "receipt_number": null,
      "starting_balance": 0,
      "statement_descriptor": null,
      "subscription": "sub_00000000000000",
      "subtotal": 16900,
      "tax": null,
      "tax_percent": null,
      "total": 16900,
      "webhooks_delivered_at": 1478115204
    }
  }
}

Create a file spec/support/stripe.rb.

StripeEvent.configure do |events|
  events.event_retriever = lambda {|params| Stripe::Event.construct_from(params) }
end

All this does is take the params and makes a Stripe event object.

Create a file spec/requests/stripe_webhook_spec.rb.

require "rails_helper"

RSpec.describe "POST /stripe-events" do
  it "ignores all events excluding invoice.payment_failed" do
    expect {
      payload = Rails.root.join("spec/fixtures/customer_subscription_created.json").read
      headers = {"Content-Type" => "application/json"}
      post "/stripe-events", payload, headers
    }.not_to change {
      ActionMailer::Base.deliveries
    }

    expect(response).to be_a_success
  end

  it "sends an email when payment fails" do
    create(:user, stripe_id: "cus_00000000000000", email: "[email protected]")

    expect {
      payload = Rails.root.join("spec/fixtures/invoice_payment_failed.json").read
      headers = {"Content-Type" => "application/json"}
      post "/stripe-events", payload, headers
    }.to change {
      ActionMailer::Base.deliveries.count
    }.by(1)

    expect(response).to be_a_success
    email = ActionMailer::Base.deliveries.last
    expect(email.to).to eq(["[email protected]"])
    expect(email.subject).to eq("Problem processing your payment")
  end
end

5. Make It Work

We now have the foundation in place for receiving the Stripe event. Time to make the tests pass. I would like to receive the invoice.payment_failed event from Stripe and each time send an email to the user asking them to update their credit card.

This is where I leave you, I could go on forever but that will have to be for another time. ;)

Photo of Denise Carpenter

Denise comes from a medical background where she was a Radiographer. A career change and four years later, she’s now a full stack web developer for Collective Idea.

Comments

  1. jfoucher@gmail.com
    Jonathan Foucher
    December 21, 2016 at 20:46 PM

    Hi Denise, thanks for the post, very interesting how you went about allowing stripe to call your local endpoint. I’ll definitely use that trick in the future.

    I’m looking to do something similar, but if I may ask : have you built your whole transactional email system that way, or just for this specific event ?

    Thanks !