How to sell code with Gumroad and Github

Sell and monetize your code by giving private access to your Github repositories using Gumroad

ยท7 min read
Cover Image for How to sell code with Gumroad and Github

Makerkit is a private repository that purchasers can access after buying a license. So, when someone buys a license, I simply add them to the private GitHub repository as soon as I see the email. This is okay, but it can usually mean half a day later for people on the other side of the world! So, I decided to automate the process using the GitHub API and the Gumroad API.

Before you ask, why not just use GitHub Sponsors? Here are a few reasons:

  1. Most people prefer purchasing using Gumroad: 90% of my sales came from there.
  2. One-off payments using GitHub do not add purchasers to your repositories by default, which was a bummer.
  3. GitHub does not offer an option to give refunds, which is important to me as I want to refund unhappy customers.
  4. Github does not handle VAT for you.

In short, GitHub Sponsors is fantastic but still a bit limited. Therefore, I also offer Gumroad as another option to purchase the kit, as it handles a lot of stuff I wouldn't want to be bothered with.

In this blog post, I will show you how to connect Gumroad and GitHub to automatically add your customers to your repositories as soon as they purchase a license using Gumroad.

Gumroad for payments, GitHub for code management

The general idea is to use Gumroad to handle payments and, optionally, build a landing page and an email list for your product.

Instead, we can leverage GitHub to distribute your code to your customers, allowing them to update the code using Git and everything else it offers (Issues, Discussions, PRs, etc.).

Creating a Personal Token with GitHub

The first step is creating a personal access token using the GitHub API. Then, using the new granular personal access tokens, we can safely make a token on a per-repository basis. Finally, we will need this token to add collaborators to the private repository using the GitHub API.

  1. Visit the page to create personal access tokens
  2. Select only the repository that you will need to add collaborators to
  3. Assign the Administration permissions to your token
  4. Generate the token, and store it safely. We will refer to this token as the GITHUB_API_TOKEN environment variable

Creating an API with Next.js

In this example, I will be using Next.js. However, don't worry if it's not your choice: the code is so simple that it should be easy to apply to other frameworks and languages.

You have two ways to handle this:

  1. Adding custom fields in your Gumroad Checkout: you will require a GitHub username to be entered, or make it optional
  2. Redirect users who chose not to provide their username can redeem their invite using the license key generated by Gumroad

So, we create two endpoints:

  1. The first will handle ping events from Gumroad after every purchase
  2. The second, instead, will handle manual submissions from your customers

Let's start!

Verifying Licenses using the Gumroad API

When creating your Gumroad product, check the option to generate license codes for each purchase.

To verify a license code, we will use the Gumroad API. The option to verify the license code is not protected by authentication, so we do not need to provide any credentials.

Below is a simple function that calls the Gumroad API to verify the license provided by the user.

The verifyGumroadLicense function below accepts an object with two properties: the product ID in Gumroad, and the license key generated after the purchase. Let's take a look:

const BASE_API = `https://api.gumroad.com`; const VERIFY_ENDPOINT = `v2/licenses/verify`; export interface GumroadLicenseResponse { success: boolean; uses: number; } interface Params { product: string; licenseKey: string; } export function verifyGumroadLicense( params: Params ): Promise<GumroadLicenseResponse> { const url = [BASE_API, VERIFY_ENDPOINT].join('/'); const headers = new Headers({ 'Content-Type': 'application/json' }); const body = JSON.stringify({ product_permalink: params.product, license_key: params.licenseKey, }); const result = await fetch(url, { method: `POST`, headers, body, }); return result.json(); }

Adding External Collaborators using the GitHub API

Let's jump onto the next part: inviting the user who purchased the license key to your private repository as an outside collaborator. To do so, we need to use the GitHub API with the personal access token generated above.

To make our life easier, we use octokit, the official library to use the GitHub API built.

import { Octokit } from 'octokit'; interface Params { owner: string; repo: string; username: string; } export function inviteMemberToRepository( params: Params ) { const octokit = new Octokit({ auth: process.env.GITHUB_API_TOKEN, }); return octokit.request('PUT /repos/{owner}/{repo}/collaborators/{username}', { owner: params.owner, repo: params.repo, username: params.username, permission: 'pull', }); }

Adding the Next.js API endpoints

Before starting, ensure you're providing the following environment variables:

GITHUB_USERNAME= GITHUB_REPOSITORY_NAME= GUMROAD_PRODUCT=

The Gumroad product's name can be found in your Gumroad Dashboard.

The function below will:

  1. verify the license exists and hasn't already been used
  2. invite the user to the private repository
async function activateLicense({ githubUsername, licenseKey, }: { githubUsername: string; licenseKey: string; }) { const owner = process.env.GITHUB_USERNAME; const repo = process.env.GITHUB_REPOSITORY_NAME; const product = process.env.GUMROAD_PRODUCT; if (!owner || !repo || !product) { return Promise.reject(`Environment variables not provided`); } console.log( { licenseKey, githubUsername, }, `License Key submitted for activation. Verifying License...` ); const license = await verifyGumroadLicense({ product, licenseKey, }); // The method below will increment "uses" by 2. // So we check if the license has already been redeemed if (license.uses > 2) { throw new ApiError(400, `License has already been redeemed`); } console.log( { licenseKey, githubUsername, }, `License Key valid. Inviting user to repository...` ); // invite member to GitHub const invite = await inviteMemberToRepository({ owner, repo, username: githubUsername, }); if (invite.data) { console.log( { licenseKey, githubUsername, }, `User successfully invited to the repository` ); } // send as much data as you need here return { repository: invite.data.repository, }; }

Responding to Ping events from Gumroad

Now that the core functions are there, we simply need to write the endpoint that will be hit by Gumroad's ping events.

We simply return a 200 status code when the API hits an unrecoverable error since Gumroad will retry the request: for example, when the required data hasn't been provided.

The endpoint below is validated using zod, but that's optional.

pages/api/license/ping.ts
interface PingPayload { custom_fields: Record<string, string>; license_key: string; } // add the key of the custom field here. In my case, it's "Github Username" const CUSTOM_FIELD_KEY = 'custom_fields[Github Username]'; async function gumroadPingHandler( req: NextApiRequest, res: NextApiResponse ) { if (req.method !== `POST`) { return res.status(501).end(); } const payload = req.body as PingPayload; const licenseKey = payload.license_key; const fields = payload.custom_fields ?? {}; const githubUsername = fields[CUSTOM_FIELD_KEY]; if (!githubUsername || !licenseKey) { console.log({ licenseKey }, `Details not provided. Exiting successfully`); res.status(200).end(); } await activateLicense({ licenseKey, githubUsername }); res.status(200).end(); } export default activateLicenseHandler;

NB: You need to add the actual key of the field as CUSTOM_FIELD_KEY. If your field's label is Github, then you will use custom_fields[Github]. In my case, it's custom_fields[Github Username].

Handling manual submissions

I also use a different endpoint to handle manual submissions that accept a POST request with the following payload:

{ githubUsername: string; licenseKey: string; }

And below is the API handler:

import { NextApiRequest, NextApiResponse } from 'next'; import { activateLicense } from '~/lib/server/activate-license'; async function activateLicenseHandler( req: NextApiRequest, res: NextApiResponse ) { if (req.method !== `POST`) { return res.status(501).end(); } const result = getBodySchema().safeParse(req.body); if (!result.success) { return res.status(400).json({ success: false }); } const body = await activateLicense(result.data); return res.json(body); } export default activateLicenseHandler; function getBodySchema() { return z.object({ githubUsername: z.string().min(1), licenseKey: z.string().min(1), }); }

Adding your endpoint to Gumroad

We've finalized the coding side of things.

All we need now is to set the endpoint to your ping function in the Gumroad API settings.

This is it! The Ping API is sorted, so your customers will be automatically added to your private repository after purchasing a license key for your products.

To handle manual submissions, you can send them a link using Gumroad's automations, so they can redeem their invite.

๐ŸŽ‰ This is it! Our automation is now complete!

Hope you this article can help you automate your business! Feel free to contact me for any questions or clarifications.


Read more about Tutorials

Cover Image for Secure One-Time Tokens with Supabase and Postgres

Secure One-Time Tokens with Supabase and Postgres

ยท5 min read
Learn how to implement robust, self-cleaning nonces using Postgres functions in your Supabase project.
Cover Image for Building Multi-Step forms with React.js

Building Multi-Step forms with React.js

ยท14 min read
In this article, we explain how to build Multi-Step forms with Next.js and the library react-hook-form
Cover Image for Mastering URL Patterns in Next.js Middleware: A Comprehensive Guide

Mastering URL Patterns in Next.js Middleware: A Comprehensive Guide

ยท5 min read
Learn how to implement and optimize URL pattern matching in Next.js middleware to create more efficient and maintainable server-side logic.
Cover Image for Building an AI Writer SaaS with Next.js and Supabase

Building an AI Writer SaaS with Next.js and Supabase

ยท57 min read
Learn how to build an AI Writer SaaS with Next.js and Supabase - from writing SEO optimized blog posts to managing subscriptions and billing.
Cover Image for Announcing the Data Loader SDK for Supabase

Announcing the Data Loader SDK for Supabase

ยท8 min read
We're excited to announce the Data Loader SDK for Supabase. It's a declarative, type-safe set of utilities to load data into your Supabase database that you can use in your Next.js or Remix apps.
Cover Image for Adding AI capabilities to your Next.js SaaS with Supabase and HuggingFace

Adding AI capabilities to your Next.js SaaS with Supabase and HuggingFace

ยท20 min read
In this tutorial, we will learn how to use add AI capabilities to your SaaS using Supabase Vector, HuggingFace models and Next.js Server Components.