# Stripe Payments webhook handler\`

**URL:** <https://forum.elixirforum.com/t/stripe-payments-webhook-handler/63761>\
**Category:** Questions\
**Tags:** stripe\
**Created:** [May 24, 2024, 11:32am UTC](https://forum.elixirforum.com/t/stripe-payments-webhook-handler/63761 "2024-05-24T11:32:58Z")\
**Posts on this page:** 17\
**Page:** 1

<div class="post-metadata">

**Author:** ![travisf](https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/travisf/32/18564_2.png) [@travisf](https://forum.elixirforum.com/u/travisf)\
**Post date:** [May 24, 2024, 11:32am UTC](https://forum.elixirforum.com/t/stripe-payments-webhook-handler/63761/1 "2024-05-24T11:32:58Z")

</div>

I’m working through [this tutorial](https://connerfritz.com/blog/stripe-webhooks-in-phoenix-with-elixir-pattern-matching) trying to get payments working with Stripe.

I’ve implemented a Stripe webhook handler:

```elixir
defmodule Postbox.StripeHandler do
  @behaviour Plug

  alias Plug.Conn

  def init(config), do: config

  def call(%{request_path: "/webhook/payments"} = conn, _params) do
    signing_secret = Application.get_env(:stripity_stripe, :webhook_key)
    [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
      err ->
        conn
        |> Conn.send_resp(:bad_request, err)
        |> Conn.halt()
    end
  end
end

```

I’ve copied my webhook secret (`:webhook_key` directly from the signing secret) but I’m still, continuously getting this error from Stripe: `** (MatchError) no match of right hand side value: {:error, "No signatures found matching the expected signature for payload"}`.

This is a test account and I’m using Ngrok for the Webhook address.

* * *

Initial question aside I’m wondering if I’m just using `Stripe.Checkout.Session` do I need to even bother with Webhooks? Willl Stripe returning a successful URL be sufficient?

---

<div class="post-metadata">

**Author:** ![al2o3cr](https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/al2o3cr/32/3457_2.png) [@al2o3cr](https://forum.elixirforum.com/u/al2o3cr)\
**Post date:** [May 24, 2024, 12:35pm UTC](https://forum.elixirforum.com/t/stripe-payments-webhook-handler/63761/2 "2024-05-24T12:35:41Z")

</div>

> [@travisf](#):
>
> ` with {:ok, body, _} = Plug.Conn.read_body(conn),`

I don’t believe it’s directly related to your issue, but note that there’s a specific callout in the [docs for `Plug.Conn.read_body`](https://hexdocs.pm/plug/Plug.Conn.html#read_body/2) saying NOT to discard the `conn`:

> Like all functions in this module, the `conn` returned by `read_body` must be passed to the next stage of your pipeline and should not be ignored.

---

<div class="post-metadata">

**Author:** ![dimitarvp](https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/dimitarvp/32/38664_2.png) [@dimitarvp](https://forum.elixirforum.com/u/dimitarvp)\
**Post date:** [May 24, 2024, 12:38pm UTC](https://forum.elixirforum.com/t/stripe-payments-webhook-handler/63761/3 "2024-05-24T12:38:59Z")

</div>

Never ignore a returned `conn`, that’s the new state you must use from then on! Start with that.

…Oh, I see @al2o3cr beat me to it.

---

<div class="post-metadata">

**Author:** ![travisf](https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/travisf/32/18564_2.png) [@travisf](https://forum.elixirforum.com/u/travisf)\
**Post date:** [May 24, 2024, 12:57pm UTC](https://forum.elixirforum.com/t/stripe-payments-webhook-handler/63761/4 "2024-05-24T12:57:21Z")

</div>

Thanks for pointing that out, I will be sure to use the new conn!

That said I don’t think this is causing the issue, I should have been more clear I believe the `with` chain is failing at `Stripe.Webhook.construct_event(body, stripe_signature, signing_secret)`. It seems like a simple authentication error but I have checked the `:webhook_key`/`:signing_secret` and it’s the one from stripe.

---

<div class="post-metadata">

**Author:** ![mize85](https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/mize85/32/27028_2.png) [@mize85](https://forum.elixirforum.com/u/mize85)\
**Post date:** [May 24, 2024, 2:04pm UTC](https://forum.elixirforum.com/t/stripe-payments-webhook-handler/63761/5 "2024-05-24T14:04:32Z")

</div>

When is your plug called?

I think it has to run before all other parsers (plug Plug.Parsers in endpoint.ex) that may change the body, to verify the body as received from stripe.

---

<div class="post-metadata">

**Author:** ![al2o3cr](https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/al2o3cr/32/3457_2.png) [@al2o3cr](https://forum.elixirforum.com/u/al2o3cr)\
**Post date:** [May 24, 2024, 7:46pm UTC](https://forum.elixirforum.com/t/stripe-payments-webhook-handler/63761/6 "2024-05-24T19:46:48Z")

</div>

The code you posted matches what’s in the `stripity_stripe` docs, so the next thing to check is all the inputs. At least one of `signing_secret`, `stripe_signature`, or `body` isn’t getting the value that `construct_event` is expecting.

One thing I’d particularly watch out for is “test mode” vs “production mode”; IIRC the test mode uses a different signing secret.

---

<div class="post-metadata">

**Author:** ![Lucassifoni](https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/lucassifoni/32/39057_2.png) [@Lucassifoni](https://forum.elixirforum.com/u/Lucassifoni)\
**Post date:** [May 25, 2024, 7:02am UTC](https://forum.elixirforum.com/t/stripe-payments-webhook-handler/63761/7 "2024-05-25T07:02:04Z")

</div>

You need to access the body before it has been modified by a subsequent Plug.

Here is an example in Plug.Parsers docs that exposes this very use case 🙂

> **[Plug.Parsers — Plug v1.20.2](https://plug.hexdocs.pm/Plug.Parsers.html)**

Edit : note that you can also do that with a custom Parser that runs before other Plug.Parsers. You can pattern match on the request path if you wish to avoid copying the raw body for other kind of requests.

```elixir
@behaviour Plug.Parsers
  alias Plug.Conn

  def parse(%{request_path: "/specific_path"} = conn, _type, _subtype, _headers, opts) do
    case Conn.read_body(conn, opts) do
      {:ok, body, conn} ->
        {:ok, %{raw_body: body}, conn}
      {:more, _data, conn} ->
        {:error, :too_large, conn}
    end
  end

  def parse(conn, _type, _subtype, _headers, _opts), do: {:next, conn}

```

Check for exhaustiveness with the docs, because I cannot verify it today.

After that, the `%{raw_body: r}` map gets merged into `conn.params`. You can access it by pattern matching on conn.params in your controller.

`def handle_webhook(%{params: %{raw_body: raw_body}} = conn, params) do`

There are many ways to get to the desired result, but the goal is the same : preserve the raw request body.

---

<div class="post-metadata">

**Author:** ![travisf](https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/travisf/32/18564_2.png) [@travisf](https://forum.elixirforum.com/u/travisf)\
**Post date:** [June 6, 2024, 1:21pm UTC](https://forum.elixirforum.com/t/stripe-payments-webhook-handler/63761/8 "2024-06-06T13:21:51Z")

</div>

I saw that but if you have `plug App.StripeHandler` _before_ `plug Plug.Parsers` wouldn’t that resolve the issue without writing a custom parser?

* * *

For what it’s worth: the payload from `Plug.Conn.read_body(conn)` looks like:

```elixir
"{\n \"id\": \"evt_3POga12ilwm20D11GsbWweX\",\n \"object\": \"event\",\n \"api_version\": \"2020-08-27\",\n \"created\": 1717681345,\n \"data\": {\n \"object\": {\n \"id\"....

```

Essentially just stringifyed JSON. Should it be raw JSON?

---

<div class="post-metadata">

**Author:** ![wojtekmach](https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/wojtekmach/32/999_2.png) [@wojtekmach](https://forum.elixirforum.com/u/wojtekmach)\
**Post date:** [June 6, 2024, 8:09pm UTC](https://forum.elixirforum.com/t/stripe-payments-webhook-handler/63761/9 "2024-06-06T20:09:00Z")

</div>

This blog post might be helpful: [https://dashbit.co/blog/how-we-verify-webhooks](https://dashbit.co/blog/how-we-verify-webhooks).

---

<div class="post-metadata">

**Author:** ![focused](https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/focused/32/23196_2.png) [@focused](https://forum.elixirforum.com/u/focused)\
**Post date:** [June 7, 2024, 11:05am UTC](https://forum.elixirforum.com/t/stripe-payments-webhook-handler/63761/10 "2024-06-07T11:05:59Z")

</div>

Look at the tutorial for `stripity_stripe`: [https://tolc.io/blog/stripe-with-elixir-and-phoenix](https://tolc.io/blog/stripe-with-elixir-and-phoenix)

Maybe you don’t need to implement a plug at all because it’s already implemented in the package? Just use the behaviours in your handler `@behaviour Stripe.WebhookHandler`.

> Initial question aside I’m wondering if I’m just using `Stripe.Checkout.Session` do I need to even bother with Webhooks? Willl Stripe returning a successful URL be sufficient?

Yes, you need to handle the event after the checkout like I did for setup payment methods:

```elixir
defmodule MyApp.Endpoint do
  plug(Stripe.WebhookPlug,
    at: "/webhook/stripe",
    handler: MyApp.PaymentService.API.Stripe.EventHandler,
    secret: {MyApp.Config, :cfg, [[:stripity_stripe, :signing_secret]]}
  )
...

```

```elixir
defmodule MyApp.PaymentService.API.Stripe.EventHandler do
  @behaviour Stripe.WebhookHandler

  @impl Stripe.WebhookHandler
  def handle_event(%Stripe.Event{type: "checkout.session.completed", data: %{object: %Stripe.Checkout.Session{mode: "setup"}}} = event) do
    ...
  end
...

```

It’s required to handle all checkout session events. Look at the Stripe API and handle your events according to entity statuses and event types.

In my case I needed to handle the [“checkout.session.completed”](https://docs.stripe.com/api/events/types?event_types-invoice.payment_succeeded=#event_types-checkout.session.completed) and then update some payment method fields in the DB to make it active in my system.

[Session object API](https://docs.stripe.com/api/checkout/sessions/object)

You should look at the events you need [https://docs.stripe.com/api/events/types#event\_types-checkout.session.async\_payment\_failed](https://docs.stripe.com/api/events/types#event_types-checkout.session.async_payment_failed) and handle them as you want, e.g. update a `Payment` schema entity status.

I would not rely on synchronous API responses only and strongly advice to handle events in a webhook.

P.S.: some useful docs and recommendations about using webhooks - [Receive Stripe events in your webhook endpoint | Stripe Documentation](https://docs.stripe.com/webhooks#webhooks-def)

---

<div class="post-metadata">

**Author:** ![travisf](https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/travisf/32/18564_2.png) [@travisf](https://forum.elixirforum.com/u/travisf)\
**Post date:** [June 28, 2024, 4:04pm UTC](https://forum.elixirforum.com/t/stripe-payments-webhook-handler/63761/11 "2024-06-28T16:04:42Z")

</div>

Thanks! The Tolc tutorail was one of several I worked off of. I’ve gutted everything I’ve done on this and copied from their tutorial directly and this is the result whenever I call the webhook in Stripe:

```elixir
[info] POST /webhook/payments
[debug] ** (Phoenix.Router.NoRouteError) no route found for POST /webhook/payments (AppWeb.Router)
    (app 0.1.0) deps/phoenix/lib/phoenix/router.ex:541: AppWeb.Router.call/2
    (app 0.1.0) lib/app_web/endpoint.ex:1: AppWeb.Endpoint.plug_builder_call/2
    (app 0.1.0) deps/plug/lib/plug/debugger.ex:136: AppWeb.Endpoint."call (overridable 3)"/2
    (app 0.1.0) lib/app_web/endpoint.ex:1: AppWeb.Endpoint.call/2
    (phoenix 1.7.11) lib/phoenix/endpoint/sync_code_reload_plug.ex:22: Phoenix.Endpoint.SyncCodeReloadPlug.do_call/4
    (bandit 1.2.1) lib/bandit/pipeline.ex:101: Bandit.Pipeline.call_plug/2
    (bandit 1.2.1) lib/bandit/pipeline.ex:22: Bandit.Pipeline.run/6
    (bandit 1.2.1) lib/bandit/http1/handler.ex:33: Bandit.HTTP1.Handler.handle_data/3
    (bandit 1.2.1) lib/bandit/delegating_handler.ex:18: Bandit.DelegatingHandler.handle_data/3
    (bandit 1.2.1) /Documents/app/deps/thousand_island/lib/thousand_island/handler.ex:379: Bandit.DelegatingHandler.handle_info/2
    (stdlib 5.2) gen_server.erl:1095: :gen_server.try_handle_info/3
    (stdlib 5.2) gen_server.erl:1183: :gen_server.handle_msg/6
    (stdlib 5.2) proc_lib.erl:241: :proc_lib.init_p_do_apply/3

[error] GenServer #PID<0.825.0> terminating
** (stop) "request line read error: \"sequence\\\": 1,\\n\""
Last message: {:tcp, #Port<0.44>, "sequence\": 1,\n \"phone\": null,\n \"preferred_locales\": [\n\n],\n \"shipping\": null,\n \"tax_exempt\": \"none\",\n \"test_clock\": null\n }\n },\n \"livemode\": false,\n \"pending_webhooks\": 2,\n \"request\": {\n \"id\": \"req_gdapqDtGAqcTo4\",\n \"idempotency_key\": \"99ba4fe5-8b3d-4a32-a543-157769480bb2\"\n },\n \"type\": \"customer.created\"\n}"}
State: {%ThousandIsland.Socket{socket: #Port<0.44>, transport_module: ThousandIsland.Transports.TCP, read_timeout: 60000, silent_terminate_on_error: false, span: %ThousandIsland.Telemetry{span_name: :connection, telemetry_span_context: #Reference<0.2255192960.2853437445.179267>, start_time: -576460639900190737, start_metadata: %{remote_port: 52323, remote_address: {127, 0, 0, 1}, telemetry_span_context: #Reference<0.2255192960.2853437445.179267>, parent_telemetry_span_context: #Reference<0.2255192960.2853437444.179063>}}}, %{opts: %{websocket: [], http_1: [], http_2: []}, plug: {Phoenix.Endpoint.SyncCodeReloadPlug, {AppWeb.Endpoint, []}}, handler_module: Bandit.HTTP1.Handler, http_1_enabled: true, http_2_enabled: true, websocket_enabled: true, requests_processed: 3}}

```

There seems to be two problems here first is that there is no route for `/webhook/payments` which is not specified in the tutorial.  
The second (related?) problem is with parsing the response, I assume if it was reaching the correct endpoint this wouldn’t be a problem (assuming everything else works correctly)?

Sorry for my slow response, I’ve been moving and this is a side project.

---

<div class="post-metadata">

**Author:** ![travisf](https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/travisf/32/18564_2.png) [@travisf](https://forum.elixirforum.com/u/travisf)\
**Post date:** [July 2, 2024, 11:41am UTC](https://forum.elixirforum.com/t/stripe-payments-webhook-handler/63761/12 "2024-07-02T11:41:02Z")

</div>

I finally got to the bottom of this, I had, occasionally, used an Ngrok tunnel as an endpoint for the webhook but mostly I was using `localhost` I didn’t realize that when you ran the Stripe CLI locally it gave you a specific `signing_secret` when the session started:

```elixir
> Ready! You are using Stripe API Version [2020-08-27]. Your webhook signing secret is whsec_..... (^C to quit)

```

I was using the signing secret from another endpoint I had setup for Ngrok.

---

<div class="post-metadata">

**Author:** ![justincjohnson](https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/justincjohnson/32/23636_2.png) [@justincjohnson](https://forum.elixirforum.com/u/justincjohnson)\
**Post date:** [January 10, 2025, 5:54pm UTC](https://forum.elixirforum.com/t/stripe-payments-webhook-handler/63761/13 "2025-01-10T17:54:58Z")

</div>

For anyone else who runs into this, the problem for me had to do with [this setting](https://github.com/mtrudel/bandit/blob/b8b56515d89a8b552c90b5f0d1d181d2bb30ff50/lib/bandit.ex#L128) in Bandit.

I was able to get around the problem by changing my config to the following.

```elixir
config :my_app_web, MyAppWeb.Endpoint,
  # ...
  http_1_options: [max_request_line_length: 50_000]

```

I haven’t thought deeply about the size to use, but this got around the error I was experiencing.

---

<div class="post-metadata">

**Author:** ![justincjohnson](https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/justincjohnson/32/23636_2.png) [@justincjohnson](https://forum.elixirforum.com/u/justincjohnson)\
**Post date:** [January 10, 2025, 7:59pm UTC](https://forum.elixirforum.com/t/stripe-payments-webhook-handler/63761/14 "2025-01-10T19:59:19Z")

</div>

Never mind. I thought it fixed it but it was intermittent. Back to the drawing board.

---

<div class="post-metadata">

**Author:** ![wceolin](https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/wceolin/32/28444_2.png) [@wceolin](https://forum.elixirforum.com/u/wceolin)\
**Post date:** [January 23, 2025, 10:42pm UTC](https://forum.elixirforum.com/t/stripe-payments-webhook-handler/63761/15 "2025-01-23T22:42:59Z")

</div>

Was your post deleted? It’s returning 404.

---

<div class="post-metadata">

**Author:** ![sorentwo](https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/sorentwo/32/37360_2.png) [@sorentwo](https://forum.elixirforum.com/u/sorentwo)\
**Post date:** [January 24, 2025, 11:36am UTC](https://forum.elixirforum.com/t/stripe-payments-webhook-handler/63761/16 "2025-01-24T11:36:16Z")

</div>

I noticed that the post appears to be deleted earlier this week. Here’s a brief, working example of how to verify stripe webhooks in phoenix.

First, parsed JSON will strip some whitespace and alphabetize keys. To calculate the correct HMAC you need to stash the original unparsed request body. A small `body_reader` module will do it:

```elixir
# body_reader.ex
defmodule MyApp.BodyReader do
  def read_body(conn, _opts) do
    # You may want to only do this for certain paths
    with {:ok, raw_body, conn} <- Plug.Conn.read_body(conn) do
      {:ok, raw_body, Plug.Conn.put_private(conn, :raw_body, raw_body)}
    end
  end
end

```

[Configure](https://hexdocs.pm/plug/1.16.1/Plug.Parsers.html#module-custom-body-reader) `Plug.Parsers` to use the new body\_reader module:

```elixir
  plug Plug.Parsers,
    parsers: [:urlencoded, :multipart, :json],
    pass: ["*/*"],
    json_decoder: Phoenix.json_library(),
    body_reader: {MyApp.BodyReader, :read_body, []}

```

Then define a private function to verify the signature in your controller:

```elixir
defmodule MyAppWeb.StripeController do
  use MyAppWeb, :controller

  plug :verify_signature

  # actions go here

  defp verify_signature(conn, _opts) do
    case get_req_header(conn, "stripe-signature") do
      [header] ->
        ["t=" <> time, "v1=" <> sigv | _] = String.split(header, ",")
        signing_secret = Application.fetch_env!(:my_app, :stripe_signing_secret)

        hmac =
          :hmac
          |> :crypto.mac(:sha256, signing_secret, [time, ".", conn.private.raw_body])
          |> Base.encode16(case: :lower)

        if Plug.Crypto.secure_compare(hmac, sigv) do
          conn
        else
          conn
          |> send_resp(400, "Invalid Signature")
          |> halt()
        end

      _ ->
        conn
        |> send_resp(400, "Missing Signature")
        |> halt()
    end
  end
end

```

That will do it! If you test your webhook controller, which you certainly should, you’ll need to inject a signature as well. The signature below is fake and computed using a test signing key (`whsec_test`), a fake timestamp, and `:raw_body`:

```elixir
  @signature "t=123456789,v1=34e0846d2ae20d2fcde8c391d069223f72d0518eaf511de69f785470938e1505"

  setup %{conn: conn} do
    conn =
      conn
      |> put_req_header("stripe-signature", @signature)
      |> put_private(:raw_body, ~s({"fake":"body"}))

    {:ok, conn: conn}
  end

```

---

<div class="post-metadata">

**Author:** ![SteveL](https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/stevel/32/35804_2.png) [@SteveL](https://forum.elixirforum.com/u/SteveL)\
**Post date:** [February 12, 2025, 3:03pm UTC](https://forum.elixirforum.com/t/stripe-payments-webhook-handler/63761/17 "2025-02-12T15:03:56Z")

</div>

Just a quick note about testing the `body_reader` that took me a while to figure out. I had a hard time just getting the custom body reader to run in controller tests until I realized the following:

- You must set a `content-type` header, and
- You have to send your post request as a string and not a map. Sending as a map will bypass the body reader.

My test ended up looking something like:

```elixir
    test "invalid hmac signature", %{conn: conn} do
      conn = 
        conn
        |> Plug.Conn.put_req_header("content-type", "application/json")
        |> post(~p"/api/webhooks", "{}")

      assert response(conn, 400) == "Invalid signature"
    end

```

Hope that helps someone stop banging their head on the desk as much as I did!
