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: fn -> Application.get_env(:myapp, :stripe_webhook_secret) end ``` """ import Plug.Conn alias Plug.Conn @behaviour Plug @impl true def init(opts) do path_info = String.split(opts[:at], "/", trim: true) opts |> Enum.into(%{}) |> Map.put_new(:path_info, path_info) end @impl true 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), {:ok, %Stripe.Event{} = event} <- construct_event(payload, signature, secret, opts), :ok <- handle_event!(handler, event) do send_resp(conn, 200, "Webhook received.") |> halt() else {:handle_error, reason} -> send_resp(conn, 400, reason) |> halt() _ -> send_resp(conn, 400, "Bad request.") |> halt() end end @impl true def call(%Conn{path_info: path_info} = conn, %{path_info: path_info}) do send_resp(conn, 400, "Bad request.") |> halt() end @impl true def call(conn, _), do: conn 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 case handler.handle_event(event) do {:ok, _} -> :ok :ok -> :ok {:error, reason} when is_binary(reason) -> {:handle_error, reason} {:error, reason} when is_atom(reason) -> {:handle_error, Atom.to_string(reason)} :error -> {:handle_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 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