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.
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.
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"
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:
Stripe-Signature
header of the request%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
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
# handle webhook
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
# handle webhook
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
# handle invoice created webhook
end
defp handle_webhook(%{type: "invoice.payment_succeeded"} = stripe_event) do
# handle invoice payment_succeeded webhook
end
defp handle_webhook(%{type: "invoice.payment_failed"} = stripe_event) do
# handle invoice payment_failed webhook
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
# handle invoice created webhook
{:ok, "success"}
end
defp handle_webhook(%{type: "invoice.payment_succeeded"} = stripe_event) do
# handle invoice payment_succeeded webhook
{:ok, "success"}
end
defp handle_webhook(%{type: "invoice.payment_failed"} = stripe_event) do
# handle invoice payment_failed webhook
{:ok, "success"}
end
end
And that's it.
If you liked this you can also follow me on Twitter, check out my GitHub, and take a look at my other blog posts.