README.org

December 29, 2024 · View on GitHub

#+BEGIN_HTML

stripe

A client for the Stripe payment API written in Common Lisp.

Github Stars License Commits-per-month ocicl

Table of Contents
  1. Getting Started
  2. Usage
  3. Coverage
  4. License
#+END_HTML

** Getting Started *** Prerequisites To use this library, you'll need:

*** Install **** Package Managers ***** [[https://www.quicklisp.org/beta/][quicklisp]] There's currently an [[https://github.com/quicklisp/quicklisp-projects/issues/2420][open PR]] to update the source location of the Stripe library, which includes updates like [[https://github.com/boogsbunny/stripe/commit/5936c43f44a197454095e1b83175dcdf3a303fd0][webhooks]].

The current version in Quicklisp doesn't include these updates yet.

#+begin_src lisp (ql:quickload :stripe) #+end_src

***** [[https://github.com/ocicl/ocicl][ocicl]]

#+begin_src bash ocicl install stripe #+end_src

**** From Source

#+begin_src bash git clone https://github.com/boogsbunny/stripe #+end_src

** Usage 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:

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 [[https://docs.stripe.com/keys][API key]] and [[https://docs.stripe.com/webhooks#verify-official-libraries][webhook signing secret]].

#+begin_src lisp (setf stripe:api-key "your-secret-api-key") (setf stripe:webhook-secret "your-webhook-signing-secret") #+end_src

Our frontend needs to include this script element:

#+begin_src html

#+end_src

After they select a tier, we want to redirect them to the checkout page. Facilitating this process is called a [[https://docs.stripe.com/api/checkout/sessions][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:

#+begin_src lisp (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))) #+end_src

Now, we'll define the add-subscription function, which creates a checkout session with Stripe and redirects the user to the appropriate URL:

#+begin_src lisp (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 "" :line-items '(("price" "" "quantity" 1)) :mode "subscription" :payment-method-types '("card") :success-url "")))) #+end_src

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:

#+begin_src lisp ;;;; 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)))))) #+end_src

Next, we'll define a macro with-parsed-json to handle JSON parsing in our webhook handler:

#+begin_src lisp (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)) #+end_src

Now, let's define the handle-webhook-event function, which validates and processes incoming webhook events from Stripe:

#+begin_src lisp (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.~%"))))) #+end_src

Lastly, we define the route to handle webhook requests:

#+begin_src lisp (snooze:defroute webhook (:post :application/json) (handle-webhook-event)) #+end_src

** Coverage This is still a work in progress. Most of the endpoints for the [[#core-resources][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.

#+BEGIN_HTML

Core Resources
Payment Methods
Products
Checkout
Payment Links
Billing
Connect
Fraud
Issuing
Terminal
Treasury
Entitlements
Sigma
Reporting
Financial Connections
Tax
Identity
Crypto
Climate
Forwarding
Webhooks
#+END_HTML

** License Distributed under the MIT License. See [[file:LICENSE][License]] for more information.