Handling Square Webhooks in Phoenix

My brother’s cafe donates a dollar to the local community centre for every coffee sold, and over the summer I built him a live “donation counter” which displays a small “thankyou” animation when anyone buys a coffee. It’s a web app which they run on an iPad sitting on the coffee machine.

Since the cafe uses Square for all payments, I was able to set up a webhook so the app would receive the “new sale” notification ASAP—this should be both lower-latency and more efficient than polling.

The app is basically a single Phoenix LiveView. Sadly the Square guides don’t have examples for Elixir, although it’s pretty easy to modify the e.g. Ruby example code to get the job done. If you’re looking to do something similar I cobbled together this info from docs (and a few blogs) and it might help you out to have it all in one place.

Step 1: set up webhook controller (including validation)

It’s important to validate that any incoming webhook is actually from Square, so Square send a special x-square-hmacsha256-signature header for validation purposes, although performing this validation step requires having access to the raw request body. Thankfully, the “Custom Body Reader” section in the Plug.Parsers docs shows how to do exactly that—just follow the instructions there.

Step 2: create webhook controller (including validation)

The webhook controller module should look something like this (replace the notification_url and signature_key with the right values for your app—you’ll get your signature key from Square when you register the webhook):

defmodule MyAppWeb.SquareWebhookController do
  @moduledoc """
  Handle webhooks sent from Square.
  """
  use MyAppWeb, :controller

  @doc "handle the webhook request"
  def webhook(conn, params) do
    if is_from_square?(conn) do
      do_stuff(params)
    end

    send_response(conn)
  end

  @doc "respond to the Square server (always 200 OK otherwise they'll freak out)"
  defp send_response(conn) do
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(200, "webhook received - thanks.")
  end

  @doc "returns `true` if webhook came from Square, `false` otherwise"
  defp is_from_square?(conn) do
    notification_url = "http://example.com/square/webhook"
    signature_key = "WEBHOOK_SIGNATURE_KEY_FROM_SQUARE"
    {_, signature} = List.keyfind!(conn.req_headers, "x-square-hmacsha256-signature", 0)

    ## here's where we access the raw request body we put there in the Plug.Parser
    raw_body = Enum.join(conn.assigns.raw_body)

    hash =
      :crypto.mac(:hmac, :sha256, signature_key, notification_url <> raw_body)
      |> Base.encode64()

    signature == hash
  end
end

Step 3: add the endpoint to your router

Finally, add it to to your router.ex - something like this, you know the drill.

scope "/square", MyAppWeb do
  pipe_through :api

  post "/webhook", SquareWebhookController, :webhook
end

Step 4: subscribe to the webhook

After that’s all done (and you’ve deployed your app) you’re ready to set up a webhook subscription. Follow the Square docs and Square will start hitting your (deployed) app’s https://example.com/square/webhook endpoint, and your app can do its thing.

Note that these incoming webhook requests won’t hit your local development server running on localhost, so testing webhooks is a bit trickier. Since my app runs on fly it involved a little bit of IO.inspecting in production and then looking at the logs with flyctl logs.

Have fun! And if you live in Canberra, especially in Tuggeranong/Lanyon, maybe go buy a coffee from Little Luxton and you can see it for yourself 😊

github twitter vimeo graduation-cap rss envelope search vial coffee heart creative-commons creative-commons-by creative-commons-nc creative-commons-sa