Tutorial

How to add a Coinbase crypto checkout to your Phoenix site

This post was updated 27 Mar

payments crypto webhook

In a previous tutorial, I showed how to add Stripe checkout in a Phoenix application. If you however want to accept Bitcoin, Ethereum or any other crypto currencies, you can implement a Coinbase hosted checkout. So, in this tutorial, I want to show you how you can get started to receive Bitcoin or Ethereum on your site.

On my site I have already implemented Stripe checkout. But even though Stripe is great, it might not work equally well in all countries and for all potential customers. So I wanted to see if I could take payments with Bitcoin, Ethereum or any other crypto currency.

As it turns out, Coinbase have a hosted checkout solution as well. I am already signed up as a private Coinbase customer.

However for this to work, I also need to sign up as a Coinbase commerce customer, which is free but they take a transaction fee of 1% which is lower than the Stripe fee.

Sign up to Coinbase to receive payments

To get started with this tutorial, you can create an account on the Coinbase commerce site. https://commerce.coinbase.com. The signup is pretty straightforward and you create a wallet for the purpose of receiving payments.

After signing up for Coinbase

After going through the signup process, I can create my first product. In Coinbase, it’s called a checkout. It is possible to sell one item at a fixed price or accept any amount of cryptocurrency with a checkout. Each charge can be generated automatically on a per-customer basis. Checkouts can contain multiple charges. You can embed payment buttons to integrate checkouts into your website. Every checkout has a public, accessible page that can easily be shared with others.

In the first step, I will pick the option “Sell a Product”

When I have gone through the process of naming the product, uploading the image and setting the price, I get a unique URL to the checkout page or code to embed.

The code for implementing the checkout button on the website looks like this:

<a class="w-full btn btn-primary"
href="https://commerce.coinbase.com/checkout/cbd2db5b-2105-4324-bfc9-6714831c3840">
<i class="bi bi-currency-bitcoin"></i>
<span class="ml-1">Crypto Checkout</span>
</a>
<script src="https://commerce.coinbase.com/v1/checkout.js?version=201807"></script>

Note that I also add the JS for Coinbase checkout. And the result on the webpage looks like this:

Crypto checkout link

Crypto checkout link

If a customer clicks the Crypto checkout button, they will be redirected to the Coinbase hosted checkout page. And there, they can choose from a number of different crypto currencies.

Coinbase hosted checkout

Coinbase hosted checkout

Receiving the webhooks from Coinbase

After Coinbase has received the payment, it can send out a webhook to a specified URL. Again, this is very similar to how Stripe does it. To receive the webhook, and save the order and payment in the database, I need to:

  1. Add Coinbase secret to the config file
  2. Create a route and controller for Coinbase to post to
  3. Make a custom plug that parses the raw post body
  4. Validate the Coinbase signing secret in the controller
  5. Parse what type of webhook it is and eventually mark the order as paid

Add Coinbase secret to the config file

The Coinbase secret is needed for receiving webhooks. The secret is received from the Coinbase interface where you set up the custom webhook endpoint.

config :my_app,
stripe_service: Stripe,
coinbase_secret: System.get_env("COINBASE_SECRET")

Create a controller and route

The first thing I need to do is to create a basic controller and a route. Since Coinbase is posting the webhooks to the specified URL, I need to set up a create action that listens to posts.

# lib/my_app_web/controllers/handle_coinbase_controller.ex
defmodule MyAppWeb.HandleCoinbaseController do
use MyAppWeb, :controller
def create(conn, payload) do
conn
|> Plug.Conn.send_resp(201, "{}")
end
end

At this point, I only want to set up a bare bone controller. In the routes file, I put in a basic route for posts and point it to the HandleCoinbaseController. The specific route is:

scope "/api", MyAppWeb do
pipe_through :api
post "/webhooks/coinbase", HandleCoinbaseController, :create
end

Note I don’t cover it in this tutorial, but to test this locally, you can register for and use ngrok so you can send webhooks to your local environment. If you do that, remember to use the URL that ngrok provides in the Coinbase settings.

Custom plug for parsing the raw body

Next step is to create a custom plug that reads the raw body that Coinbase posts. I need to do this because Phoenix parses the body and makes minor changes to it, and since I verify the payload with a hashing algorithm, I need to use the body before Phoenix changes it.

Basically, I just need to grab the raw body and the webhook signature and assign it in plug assigns. I have the plug looking like this:

# lib/my_app_web/plugs/parse_raw_body.ex
defmodule MyAppWeb.Plugs.ParseRawBody do
import Plug.Conn, only: [get_req_header: 2, assign: 3, read_body: 1]
def init(options), do: options
def call(conn, _opts) do
case get_signature(conn) do
[] ->
conn
["" <> signature] ->
{:ok, raw_body, _conn} = read_body(conn)
conn
|> assign(:raw_body, raw_body)
|> assign(:signature, signature)
end
end
defp get_signature(conn) do
get_req_header(conn, "x-cc-webhook-signature")
end
end

And in the endpoint.ex, make sure to add the plug before Plug.Parsers:

# lib/my_app_web/endpoint.ex
# BEFORE plug Plug.Parsers
plug MyAppWeb.Plugs.ParseRawBody
plug Plug.Parsers

This now means that all incoming webhooks have their signature and body assigned to conn assigns.

Validate the Coinbase signing secret in the controller

In the controller, I need to compare the signature sent in the header with a calculated signature to assert that the post is actually from Coinbase and can be trusted. Every Coinbase Commerce webhook request includes an X-CC-Webhook-Signature header. This header contains the SHA256 HMAC signature of the raw request payload, computed using your webhook shared secret as the key.

The controller code for this logic looks like this:

# lib/my_app_web/controllers/handle_coinbase_controller.ex
defmodule MyAppWeb.HandleCoinbaseController do
use MyAppWeb, :controller
require Logger
plug :verify_header
def create(conn, payload) do
conn
|> Plug.Conn.send_resp(201, "{}")
end
defp verify_header(conn, _) do
with "" <> signature <- conn.assigns[:signature],
"" <> raw_body <- conn.assigns[:raw_body],
true <- compare_signature(signature, raw_body) do
conn
else
_ ->
conn
|> send_resp(401, "unauthorized")
|> halt()
end
end
defp compare_signature(signature, raw_body) do
secret = Application.get_env(:my_app, :coinbase_secret) || ""
signature ==
:crypto.mac(:hmac, :sha256, secret, raw_body)
|> Base.encode16(case: :lower)
end
end

Related Tutorials

Published 23 Sep - 2020
Updated 27 Mar

Setup Stripe with Phoenix LiveView

In this tutorial, I will go through how I setup Stripe payments with Phoenix and LiveView to make your app prepared for accepting payments. The tuto..