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:
- Most people prefer purchasing using Gumroad: 90% of my sales came from there.
- One-off payments using GitHub do not add purchasers to your repositories by default, which was a bummer.
- GitHub does not offer an option to give refunds, which is important to me as I want to refund unhappy customers.
- 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.
- Visit the page to create personal access tokens
- Select only the repository that you will need to add collaborators to
- Assign the
Administration
permissions to your token - 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:
- Adding custom fields in your Gumroad Checkout: you will require a GitHub username to be entered, or make it optional
- 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:
- The first will handle
ping
events from Gumroad after every purchase - 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:
- verify the license exists and hasn't already been used
- 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.
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.