Skip to content

boogsbunny/stripe

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

55 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

Getting Started

Prerequisites

To use this library, you’ll need:

Install

Package Managers

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

From Source

git clone https://github.com/boogsbunny/stripe

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:

  • 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))

Coverage

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
Payment Methods
Products
Checkout
Payment Links
Billing
Connect
Fraud
Issuing
Terminal
Treasury
Entitlements
Sigma
Reporting
Financial Connections
Tax
Identity
Crypto
Climate
Forwarding
Webhooks

License

Distributed under the MIT License. See License for more information.