Add billing to your Common Lisp app
Most web applications you want to build will include billing at some point. While it’s relatively straightforward to incorporate Stripe using one of their official SDKs, it isn’t for languages like Common Lisp. Sure, you can just handle everything via the REST API directly but chances are that if you’re just starting out with Common Lisp that you already have your hands full grokking other concepts and want to write minimal code for basic things. Ideally, we’d have some resource that you can look over and copy-paste.
Well, sorry to burst your bubble, but said resources don’t exist. I tried to look up how others have added billing to their projects but I couldn’t find anything.
The most common sentiment I’ve read is that it’s not hard and most just interact with the API directly. Sure, it’s not hard but sharing how others go on about building their projects would be helpful to newcomers.
So I’m going to share how I approached it.
Libraries
First things first, we want to see which libraries are available for our use case. As it turns out, this Awesome Common Lisp repo is a great resource for finding good libraries that are used within the community.
Under the Third-party APIs section we’ll find that there are two Stripe libraries. Yes, just two, although neither is polished and one is a wrapper around various payment processors. There are more libraries available outside of this list but they’re not working with the latest API version.
I would recommend the other library, which was originally created by Michael Fiano. However, he has since quit the community, and I’ve taken over maintenance. This transition allows us to bring the library up to speed with the latest API features, including webhook support.
Installation
From source
git clone https://github.com/boogsbunny/stripe
Requirements
You have to have a Stripe account with an API key and should read through how to receive Stripe events in your webhook endpoint.
Minimal example
These are the libraries we’ll be using to get started:
- babel: This is a charset encoding/decoding library. It helps with converting octets (byte vectors) to strings and vice versa. In this example, it’s used to decode JSON webhook payloads from Stripe.
- com.inuoe.jzon: This is a JSON reader/writer. It parses JSON data from incoming webhook requests and serializes JSON when needed.
- flexi-streams: A flexible library that allows efficient reading from and writing to streams, particularly useful for handling binary data. It’s used here for stream-related parsing of webhook payloads.
- snooze: A minimalistic web framework for routing and handling HTTP requests. It’s used to define our API routes, including handling webhooks.
- stripe: The client library we chose for interacting with the Stripe API, including creating checkout sessions and validating webhook events.
Handling environment variables
Before diving into the billing flow, let’s first store the Stripe API key and webhook signing secret.
We’ll do this by reading in the environment variables that you’ll have to set.
(defmacro env-var (name var)
"Define a function that retrieves the value of an environment variable."
`(defun ,name ()
(or (uiop:getenv ,var) "")))
(env-var stripe-secret-key "STRIPE_SECRET_KEY")
(env-var stripe-webhook-secret "STRIPE_WEBHOOK_SECRET")
Next, we need to set the API key:
(setf stripe:*api-key* (stripe-secret-key))
Billing flow
Let’s focus on user subscriptions specifically, where customers are making recurring payments to access your product. Here’s an article by Stripe on how subscriptions work.
I’ll keep it simple for my use case. I have a landing page with a pricing section describing different tiers of my product with varying price points. The user can click on any of these sections to subscribe to that tier.
Our frontend needs to include this script element:
<script src=https://js.stripe.com/v3/></script>
After they select a tier, we want to redirect them to the checkout page. Facilitating this process is called a session. We need to add buttons for each subscription tier that hit our API endpoint to redirect them to our session URL.
Here’s the function that handles the redirection:
(defun redirect-to (url &optional (format-control "Redirected") format-args)
"Redirects the client to the specified URL with an optional message."
(setf (getf snooze::*clack-response-headers* :location) url)
(snooze:http-condition 302 (format nil "~?" format-control format-args)))
Now, we’ll define the `add-subscription` function, which creates a checkout session with Stripe and redirects the user to the appropriate URL:
(defun add-subscription ()
"Redirects the user to the Stripe checkout session URL for the selected plan."
(redirect-to
(stripe:session-url
(stripe:create-session
:cancel-url "<your-cancel-url>"
:line-items '(("price" "<price-id>" "quantity" 1))
:mode "subscription"
:payment-method-types '("card")
:success-url "<your-success-url>"))))
Stripe provides webhook notifications to inform your application about events like payments or subscription status changes. We need to handle these events by processing the incoming JSON data.
Let’s start by defining a utility function `parse-stream` that reads the contents of a stream and returns it as a vector of unsigned bytes:
;;;; Original code provided by Eitaro Fukamachi.
;;;; Copyright (c) 2014 Eitaro Fukamachi
;;;; github.com/fukamachi/http-body
(defun parse-stream (stream &optional content-length)
"Reads the contents of a stream and returns it as a vector of unsigned bytes.
- `stream`: The input stream from which to read.
- `content-length`: If provided, specifies the exact number of bytes to read."
(if (typep stream 'flexi-streams:vector-stream)
(coerce (flexi-streams::vector-stream-vector stream) '(simple-array (unsigned-byte 8) (*)))
(if content-length
(let ((buffer (make-array content-length :element-type '(unsigned-byte 8))))
(read-sequence buffer stream)
buffer)
(apply #'concatenate
'(simple-array (unsigned-byte 8) (*))
(loop with buffer = (make-array 1024 :element-type '(unsigned-byte 8))
for read-bytes = (read-sequence buffer stream)
collect (subseq buffer 0 read-bytes)
while (= read-bytes 1024))))))
Next, we’ll define a macro `with-parsed-json` to handle JSON parsing in our webhook handler:
(defmacro with-parsed-json (&body body)
"Parses the JSON body of an incoming HTTP request and binds it to a local
variable `json`.
Within BODY, the variable `json` will contain the parsed JSON object."
`(let* ((content-type (getf snooze:*clack-request-env* :content-type))
(content-length (getf snooze:*clack-request-env* :content-length))
(raw-body (getf snooze:*clack-request-env* :raw-body))
(json-stream (parse-stream raw-body content-length))
(raw-json (babel:octets-to-string json-stream
:encoding (detect-charset content-type :utf-8)))
(json (handler-case (com.inuoe.jzon:parse raw-json)
(error (e)
(format t "Malformed JSON (~a)~%!" e)
(http-condition 400 "Malformed JSON!")))))
(declare (ignorable json))
,@body))
Now, let’s define the `handle-webhook-event` function, which validates and processes incoming webhook events from Stripe:
(defun handle-webhook-event ()
"Handles incoming webhook events from Stripe webhooks."
(with-parsed-json
(let* ((is-valid-webhook (stripe:validate-webhook-payload
json-stream
(gethash "stripe-signature" (getf snooze:*clack-request-env* :headers))
(stripe-webhook-secret)))
(event (stripe:construct-webhook-event
json-stream
(gethash "stripe-signature" (getf snooze:*clack-request-env* :headers))
(stripe-webhook-secret)
:ignore-api-version-mismatch t))
(event-type (gethash "type" json)))
(if is-valid-webhook
(progn
(format t "Valid webhook received.~%")
(cond ((string= "payment_intent.created" event-type)
(format t "Payment intent created!~%")
;; TODO: Proceed with creating a user or processing the payment intent here
)
((string= "customer.subscription.created" event-type)
(format t "Subscription created!~%")
;; TODO: Handle subscription creation
)
((string= "invoice.payment_succeeded" event-type)
(format t "Payment succeeded for invoice!~%")
;; TODO: Handle the successful payment
)
;; etc.
(t
(format t "Unhandled event type: ~a~%" event-type))))
(format t "Invalid webhook signature.~%")))))
Lastly, we define the route to handle webhook requests:
(snooze:defroute webhook (:post :application/json)
(handle-webhook-event))
That’s it. I hope I left you off in a better position than before and you have a clearer idea of how to add billing to your next Common Lisp app.