Table of Contents
To use this library, you’ll need:
- A Stripe account with API keys.
- You can sign up at Stripe Signup.
- Obtain your API keys from the Stripe Dashboard.
There’s currently an open PR to update the source location of the Stripe library, which includes updates like webhooks.
The current version in Quicklisp doesn’t include these updates yet.
(ql:quickload :stripe)
ocicl install stripe
git clone https://github.com/boogsbunny/stripe
Here’s a minimal example of how to use the library to create a session and handle webhook events.
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 URL routing library handling HTTP requests. It’s used to define our API routes, including handling webhooks.
We’ll keep it simple for this use case. Imagine we have a landing page with a pricing section describing different tiers of a product with varying price points. The user can click on any of these sections to subscribe to that tier.
First we’ll first store the Stripe API key and webhook signing secret.
(setf stripe:*api-key* "your-secret-api-key")
(setf stripe:*webhook-secret* "your-webhook-signing-secret")
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)) ; WIP to get our library up to date
(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))
This is still a work in progress. Most of the endpoints for the Core Resources section are implemented, although some need to be updated for full parity with the Stripe API. Each endpoint will be marked off once it reaches parity, including details like all object attributes and complete endpoint coverage.
Core Resources
- ☑ Balance
- ☑ Balance Transactions
- ☑ Charges
- ☐ Customers
- ☐ Customer Session
- ☐ Disputes
- ☐ Events
- ☐ Events v2
- ☐ Files
- ☐ File Links
- ☐ Mandates
- ☐ Payment Intents
- ☐ Setup Intents
- ☐ Setup Attempts
- ☐ Payouts
- ☐ Refunds
- ☐ Confirmation Token
- ☐ Tokens
Payment Methods
Products
- ☐ Products
- ☐ Prices
- ☐ Coupons
- ☐ Promotion Code
- ☐ Discounts
- ☐ Tax Code
- ☐ Tax Rate
- ☐ Shipping Rates
Checkout
- ☐ Sessions
Payment Links
Billing
- ☐ Credit Note
- ☐ Customer Balance Transaction
- ☐ Customer Portal Session
- ☐ Customer Portal Configuration
- ☐ Invoices
- ☐ Invoice Items
- ☐ Invoice Line Item
- ☐ Invoice Rendering Templates
- ☐ Alerts
- ☐ Meters
- ☐ Meter Events
- ☐ Meter Events v2
- ☐ Meter Event Adjustment
- ☐ Meter Event Adjustment v2
- ☐ Meter Event Stream v2
- ☐ Meter Event Summary
- ☐ Plans
- ☐ Quote
- ☐ Subscriptions
- ☐ Subscription Items
- ☐ Subscription Schedule
- ☐ Tax IDs
- ☐ Test Clocks
- ☐ Usage Records
- ☐ Usage Record Summary
Connect
Fraud
Issuing
Terminal
Treasury
Entitlements
Sigma
Reporting
Financial Connections
- ☐ Accounts
- ☐ Account Owner
- ☐ Session
- ☐ Transactions
Identity
Crypto
Climate
Forwarding
Webhooks
Distributed under the MIT License. See License for more information.