if Code.ensure_loaded?(Plug) do defmodule Stripe.WebhookPlug do @moduledoc """ Helper `Plug` to process webhook events and send them to a custom handler. ## Installation To handle webhook events, you must first configure your application's endpoint. Add the following to `endpoint.ex`, **before** `Plug.Parsers` is loaded. ```elixir plug Stripe.WebhookPlug, at: "/webhook/stripe", handler: MyAppWeb.StripeHandler, secret: "whsec_******" ``` If you have not yet added a webhook to your Stripe account, you can do so by visiting `Developers > Webhooks` in the Stripe dashboard. Use the route you configured in the endpoint above and copy the webhook secret into your app's configuration. ### Supported options - `at`: The URL path your application should listen for Stripe webhooks on. Configure this to match whatever you set in the webhook. - `handler`: Custom event handler module that accepts `Stripe.Event` structs and processes them within your application. You must create this module. - `secret`: Webhook secret starting with `whsec_` obtained from the Stripe dashboard. This can also be a function or a tuple for runtime configuration. - `tolerance`: Maximum age (in seconds) allowed for the webhook event. See `Stripe.Webhook.construct_event/4` for more information. ## Handling events You will need to create a custom event handler module to handle events. Your event handler module should implement the `Stripe.WebhookHandler` behavior, defining a `handle_event/1` function which takes a `Stripe.Event` struct and returns either `{:ok, term}` or `:ok`. This will mark the event as successfully processed. Alternatively handler can signal an error by returning `:error` or `{:error, reason}` tuple, where reason is an atom or a string. HTTP status code 400 will be used for errors. ### Example ```elixir # lib/myapp_web/stripe_handler.ex defmodule MyAppWeb.StripeHandler do @behaviour Stripe.WebhookHandler @impl true def handle_event(%Stripe.Event{type: "charge.succeeded"} = event) do # TODO: handle the charge.succeeded event end @impl true def handle_event(%Stripe.Event{type: "invoice.payment_failed"} = event) do # TODO: handle the invoice.payment_failed event end # Return HTTP 200 for unhandled events @impl true def handle_event(_event), do: :ok end ``` ## Configuration You can configure the webhook secret in your app's own config file. For example: ```elixir config :myapp, # [...] stripe_webhook_secret: "whsec_******" ``` You may then include the secret in your endpoint: ```elixir plug Stripe.WebhookPlug, at: "/webhook/stripe", handler: MyAppWeb.StripeHandler, secret: Application.get_env(:myapp, :stripe_webhook_secret) ``` ### Runtime configuration If you're loading config dynamically at runtime (eg with `runtime.exs` or an OTP app) you must pass a tuple or function as the secret. ```elixir # With a tuple plug Stripe.WebhookPlug, at: "/webhook/stripe", handler: MyAppWeb.StripeHandler, secret: {Application, :get_env, [:myapp, :stripe_webhook_secret]} # Or, with a function plug Stripe.WebhookPlug, at: "/webhook/stripe", handler: MyAppWeb.StripeHandler, secret: &MyAppWeb.Secrets.stripe_webhook_secret/0 # a remote function in the format &Mod.fun/arity ``` """ @behaviour Plug import Plug.Conn alias Plug.Conn @impl Plug def init(opts) do path_info = String.split(opts[:at], "/", trim: true) opts |> Enum.into(%{}) |> Map.put_new(:path_info, path_info) end @impl Plug def call( %Conn{method: "POST", path_info: path_info} = conn, %{ path_info: path_info, secret: secret, handler: handler } = opts ) do secret = parse_secret!(secret) with [signature] <- get_req_header(conn, "stripe-signature"), {:ok, payload, conn} <- Conn.read_body(conn) do process_event(conn, payload, signature, secret, handler, opts) else _ -> halt(send_resp(conn, 400, "Bad request.")) end end @impl Plug def call(%Conn{path_info: path_info} = conn, %{path_info: path_info}) do halt(send_resp(conn, 400, "Bad request.")) end @impl Plug def call(conn, _), do: conn # Body has been read; `conn` here is the post-`read_body` conn passed as a # parameter, so it stays in scope for both the success and error branches. # Sending the response on the pre-`read_body` conn confuses Bandit's # connection state on keep-alive HTTP/1.1 and can corrupt the next request. defp process_event(conn, payload, signature, secret, handler, opts) do with {:ok, %Stripe.Event{} = event} <- construct_event(payload, signature, secret, opts), :ok <- handle_event!(handler, event) do halt(send_resp(conn, 200, "Webhook received.")) else {:handle_error, reason} -> halt(send_resp(conn, 400, reason)) _ -> halt(send_resp(conn, 400, "Bad request.")) end end defp construct_event(payload, signature, secret, %{tolerance: tolerance}) do Stripe.Webhook.construct_event(payload, signature, secret, tolerance) end defp construct_event(payload, signature, secret, _opts) do Stripe.Webhook.construct_event(payload, signature, secret) end defp handle_event!(handler, %Stripe.Event{} = event) do telemetry_meta = %{event: event.type, handler_status: nil} :telemetry.span(~w[stripe webhook]a, telemetry_meta, fn -> case handler.handle_event(event) do {:ok, _} -> {:ok, %{telemetry_meta | handler_status: :ok}} :ok -> {:ok, %{telemetry_meta | handler_status: :ok}} {:error, reason} when is_binary(reason) -> {{:handle_error, reason}, %{telemetry_meta | handler_status: :error}} {:error, reason} when is_atom(reason) -> {{:handle_error, Atom.to_string(reason)}, %{telemetry_meta | handler_status: :error}} :error -> {{:handle_error, ""}, %{telemetry_meta | handler_status: :error}} resp -> raise """ #{inspect(handler)}.handle_event/1 returned an invalid response. Expected {:ok, term}, :ok, {:error, reason} or :error Got: #{inspect(resp)} Event data: #{inspect(event)} """ end end) end defp parse_secret!({m, f, a}), do: apply(m, f, a) defp parse_secret!(fun) when is_function(fun), do: fun.() defp parse_secret!(secret) when is_binary(secret), do: secret defp parse_secret!(secret) do raise """ The Stripe webhook secret is invalid. Expected a string, tuple, or function. Got: #{inspect(secret)} If you're setting the secret at runtime, you need to pass a tuple or function. For example: plug Stripe.WebhookPlug, at: "/webhook/stripe", handler: MyAppWeb.StripeHandler, secret: {Application, :get_env, [:myapp, :stripe_webhook_secret]} """ end end end