In life, there are some perfect pairings: wine and cheese, peanut butter and jelly, chicken and waffles, scotch and depression. Some things just work really well together. Luckily for us, one of those things (in my opinion) is Stripe Webhooks and Pattern Matching in Elixir.

Despite this, consuming Stripe Webhook Events with Phoenix is not without its pitfalls. That's why I wanted to write this tutorial; to help people navigate those pitfalls in a way that I think utilizes the power of Elixir and Phoenix in a friendly way.

So, without further blabbering, let's get into it.

Quick Note:

Before we start, there's two weird things I do in this tutorial. First, I don't use import or alias much to keep it clear where the methods I'm using are coming from. Also, I build out large files additively step-by-step, explaining the reasoning behind the lines we add, but I don't trim or abbreviate the repeated lines from previous steps, this way the entire file can be copy and pasted into a project by those who want/need to.

What you'll need

Prologue: Routes and Configuration

First thing we need to do to get this show on the road is to set up a webhook route where we will receive Stripe Webhook Events. I put it in its own scope outside of the browser and other pipelines to keep things simple.

lib/my_app_web/router.ex

scope "/stripe/webhooks" do post "/" , WebhooksController , :webhooks end

Later on, we'll create the WebhooksController that this route references, so don't worry about it for now.

Now, we need to add our Webhook Signing Secret to our config. You get this value from your Webhook Dashboard page. I like to keep this alongside the other Stripity Stripe config. Both this secret and the API key should not be committed to GitHub, and you should use your preferred best practices for configuring, handling and storing application secrets.

config/config.exs

config :stripity_stripe , api_key: "SECRET KEY" , signing_secret: "WEBHOOK SIGNING SECRET"

Chapter One: Plug It Up

The first pitfall one encounters with Phoenix has to do with the body_parser used by Plug.Parsers in your endpoint.ex file. The default body parser deletes the raw request body. Problem is, we need that raw request body alongside the signing secret to verify the authenticity of the Stripe Webhook Event; otherwise anyone could call our Webhook endpoint and cause all sorts of payment mayhem.

I like to work around this by adding a custom Plug before the Plug.Parsers run.

lib/my_app_web/endpoint.ex

plug MyAppWeb . StripeWebhooksPlug plug Plug . Parsers , parsers: [ :urlencoded , :multipart , :json ] , ...

Then, we create a StripeWebhooksPlug which will do a few useful things for us:

Read the body of the request Read the Stripe-Signature header of the request Verify the authenticity of the Stripe Webhook Event Attach the verified %StripeEvent{} object to the conn

BUT only on requests to our webhooks route.

Let's handle the but condition first: we only want this to do things to requests to our webhooks route (defined as /stripe/webhooks earlier). Let's use pattern matching to accomplish this:

lib/my_app_web/plugs/stripe_webhooks_plug.ex

defmodule MyAppWeb . StripeWebhooksPlug do @behaviour Plug def init ( config ) , do: config def call ( % { request_path: "/stripe/webhooks" } = conn , _ ) do conn end def call ( conn , _ ) , do: conn end

With pattern matching, the top method will handle all requests whose path matches our webhook route. The second one will handle everything else by just returning the conn unchanged.

Now, let's make the method read the body of the request. We're also going to get our signing secret from our config file.

stripe_webhooks_plug.ex (CONTINUED)

defmodule MyAppWeb . StripeWebhooksPlug do @behaviour Plug def init ( config ) , do: config def call ( % { request_path: "/stripe/webhooks" } = conn , _ ) do signing_secret = Application . get_env ( :stripity_stripe , :signing_secret ) { :ok , body , _ } = Plug . Conn . read_body ( conn ) conn end def call ( conn , _ ) , do: conn end

Now we have the body of the request stored as body and the signing secret stored as signing_secret for later use.

Next, let's do the same for the Stripe-Signature header, which we also need so we can verify the authenticity of the request.

stripe_webhooks_plug.ex (CONTINUED)

defmodule MyAppWeb . StripeWebhooksPlug do @behaviour Plug def init ( config ) , do: config def call ( % { request_path: "/stripe/webhooks" } = conn , _ ) do signing_secret = Application . get_env ( :stripity_stripe , :signing_secret ) { :ok , body , _ } = Plug . Conn . read_body ( conn ) [ stripe_signature ] = Plug . Conn . get_req_header ( conn , "stripe-signature" ) conn end def call ( conn , _ ) , do: conn end

Now we have the signature stored as stripe_signature for later use.

Now we have everything we need to verify the webhook request, so let's do that using Stripe.Webhook.construct_event/3 from Stripity Stripe.

stripe_webhooks_plug.ex (CONTINUED)

defmodule MyAppWeb . StripeWebhooksPlug do @behaviour Plug def init ( config ) , do: config def call ( % { request_path: "/stripe/webhooks" } = conn , _ ) do signing_secret = Application . get_env ( :stripity_stripe , :signing_secret ) { :ok , body , _ } = Plug . Conn . read_body ( conn ) [ stripe_signature ] = Plug . Conn . get_req_header ( conn , "stripe-signature" ) { :ok , stripe_event } = Stripe . Webhook . construct_event ( body , stripe_signature , signing_secret ) conn end def call ( conn , _ ) , do: conn end

Stripe.Webhook.construct_event/3 verifies the request is legitimate and then returns a constructed %Stripe.Event{} , which we capture as stripe_event .

The last thing to do is to assign that to the conn with Plug.Conn.assign/3 .

stripe_webhooks_plug.ex (CONTINUED)

defmodule MyAppWeb . StripeWebhooksPlug do @behaviour Plug def init ( config ) , do: config def call ( % { request_path: "/stripe/webhooks" } = conn , _ ) do signing_secret = Application . get_env ( :stripity_stripe , :signing_secret ) { :ok , body , _ } = Plug . Conn . read_body ( conn ) [ stripe_signature ] = Plug . Conn . get_req_header ( conn , "stripe-signature" ) { :ok , stripe_event } = Stripe . Webhook . construct_event ( body , stripe_signature , signing_secret ) Plug . Conn . assign ( conn , :stripe_event , stripe_event ) end def call ( conn , _ ) , do: conn end

You'll probably want to add some logic to handle error cases in your preferred way, but here's an example of what you could do:

stripe_webhooks_plug.ex (CONTINUED)

defmodule MyAppWeb . StripeWebhooksPlug do @behaviour Plug import Plug . Conn def init ( config ) , do: config def call ( % { request_path: "/stripe/webhooks" } = conn , _ ) do signing_secret = Application . get_env ( :stripity_stripe , :signing_secret ) [ stripe_signature ] = Plug . Conn . get_req_header ( conn , "stripe-signature" ) with { :ok , body , _ } = Plug . Conn . read_body ( conn ) , { :ok , stripe_event } = Stripe . Webhook . construct_event ( body , stripe_signature , signing_secret ) do Plug . Conn . assign ( conn , :stripe_event , stripe_event ) else { :error , error } -> conn |> send_resp ( :bad_request , error . message ) |> halt ( ) end end def call ( conn , _ ) , do: conn end

Chapter Two: Controlling It

Now, let's go about creating our WebhooksController that we referenced at the beginning of the tutorial in the router.ex file. At its core, it should look like this:

lib/my_app_web/controllers/webhooks_controller.ex

defmodule WebhooksController do use MyAppWeb , :controller def webhooks ( conn , _params ) do end end

Now, let's use some pattern matching to grab the stripe_event we assigned to the conn in the StripeWebhooksPlug .

webhooks_controller.ex (CONTINUED)

defmodule WebhooksController do use MyAppWeb , :controller def webhooks ( % Plug . Conn { assigns: % { stripe_event: stripe_event } } = conn , _params ) do end end

Then we'll add the handle_webhook methods using pattern matching and call them from the webhooks method.

webhooks_controller.ex (CONTINUED)

defmodule WebhooksController do use MyAppWeb , :controller def webhooks ( % Plug . Conn { assigns: % { stripe_event: stripe_event } } = conn , _params ) do handle_webhook ( stripe_event ) end defp handle_webhook ( % { type: "invoice.created" } = stripe_event ) do end defp handle_webhook ( % { type: "invoice.payment_succeeded" } = stripe_event ) do end defp handle_webhook ( % { type: "invoice.payment_failed" } = stripe_event ) do end end

What's happening here? Well, we've got our webhooks/1 method that is called by the router. It then passes the stripe_event it receives to handle_webhook/1 . Each definition of handle_webhook/1 only responds to different versions of the stripe_event argument, depending on the value of type . A %Stripe.Event{} object has a different type depending on the Stripe Webhook Event it received, so by pattern matching against that field, we can define different versions of the handle_webhook/1 method for each event.

Finally, let's make sure we're responding correctly to Stripe to let them know we received and successfully handled their event.

webhooks_controller.ex (CONTINUED)

defmodule WebhooksController do use MyAppWeb , :controller def webhooks ( % Plug . Conn { assigns: % { stripe_event: stripe_event } } = conn , _params ) do case handle_webhook ( stripe_event ) do { :ok , _ } -> handle_success ( conn ) { :error , error } -> handle_error ( conn , error ) _ -> handle_error ( conn , "error" ) end end defp handle_success ( conn ) do conn |> put_resp_content_type ( "text/plain" ) |> send_resp ( 200 , "ok" ) end defp handle_error ( conn , error ) do conn |> put_resp_content_type ( "text/plain" ) |> send_resp ( 422 , error ) end defp handle_webhook ( % { type: "invoice.created" } = stripe_event ) do { :ok , "success" } end defp handle_webhook ( % { type: "invoice.payment_succeeded" } = stripe_event ) do { :ok , "success" } end defp handle_webhook ( % { type: "invoice.payment_failed" } = stripe_event ) do { :ok , "success" } end end

And that's it.