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.exscope "/stripe/webhooks" do
post "/", WebhooksController, :webhooks
endLater 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.exsconfig :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.explug 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 connBUT 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.exdefmodule 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
endWith 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
endNow 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
endNow 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
endStripe.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
endYou'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
endNow, 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.exdefmodule WebhooksController do
use MyAppWeb, :controller
def webhooks(conn, _params) do
# handle webhook
end
endNow, 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
endThen 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
endWhat'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
endAnd 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.