I'm in the process of migrating from Gumroad to Lemon Squeezy, and as part of it, I'm also migrating the licensing system to be able to redeem an invite to the Makerkit's Github organization.
Since I'm not the only one who wants to do this, I thought I'd write a quick tutorial on how to do it.
In this tutorial we'll learn how to:
- Using the Lemon Squeezy API to validate and activate a license
- Using the Github Octokit library to invite a user to a Github organization
- Creating a Next.js endpoint to handle the request
- Using the redirect feature of Lemon Squeezy to include the license key in the redirect URL
The tweet below shows the final result:
<p lang="en" dir="ltr"> 🤩 New <a href="https://twitter.com/makerkit_dev?ref_src=twsrc%5Etfw">@makerkit_dev</a> checkout flow powered by <a href="https://twitter.com/lmsqueezy?ref_src=twsrc%5Etfw">@lmsqueezy</a> 🎉Take a look 👇Disclaimer: I had no idea they also added confetti; I think I'll have to remove it 🤣<a href="https://twitter.com/hashtag/buildinpublic?src=hash&ref_src=twsrc%5Etfw">#buildinpublic</a> <a href="https://t.co/aaB0VIW9m7">pic.twitter.com/aaB0VIW9m7</a> </p>
— Giancarlo (@gc_psk) <a href="https://twitter.com/gc_psk/status/1616094800239038473?ref_src=twsrc%5Etfw">January 19, 2023</a>
Why move from Gumroad to Lemon Squeezy?
I've been using Gumroad for a while now, and I'm happy with it. It's a great platform to sell digital products, and I've been using it for a while now.
Unfortunately, the fees hike to 13% is way too much to justify since I only use the checkout, and not the other features. Lemon Squeezy is a great alternative, which will lower the fees to to 5% (6.5% for international payments).
How we can use Lemon Squeezy to sell code
Makerkit offers access to the premium kits using private GitHub repositories. To allow customers to get an invite after checkout, we redirect customers to a page where they will enter their Github username. We verify that the customer has purchased the product, and has enough seats available, and then we send an invite to the Github organization.
At Makerkit we sell two plans:
- a solo-developer plan, which allows access to all the kits, but only for one developer
- a team plan, which allows access to all the kits, and allows to add up to 5 developers
The quantity of available activations is set in the Lemon Squeezy dashboard, and we can change it at any time. We will use this quantity to verify that the customer has enough activations available.
The Lemon Squeezy Licensing API
Lemon Squeezy provides a licensing API, which allows us to verify that a customer has purchased a product, and has enough activations available.
Let's write some quick functions that allows us to:
- Verify the customer has purchased the product
- Verify the customer has enough activations available
- Activate the license for a specific Github username
Adding the Lemon Squeezy API types
Below are the TypeScript types for the Lemon Squeezy API. We will use them to type the functions we will write.
export interface LicenseKey { valid: boolean; error: null; license_key: { id: number; status: string; key: string; activation_limit: number; activation_usage: number; created_at: string; expires_at: null; test_mode: boolean; }; instance: null; meta: { store_id: 9810; order_id: 373672; order_item_id: 373912; product_id: 42256; variant_id: 37011; };}export interface ActivateLicenseResponse { activated: boolean; error: null; license_key: { id: number; status: number; key: string; activation_limit: number; activation_usage: number; created_at: string; expires_at: null | string; }; instance: { id: string; name: string; created_at: string; }; meta: { store_id: number; order_id: number; order_item_id: number; product_id: number; variant_id: number; };}
The API functions to verify and activate the license
Now, we can use the types defined above to write the functions that will verify and activate the license.
We create two functions:
getLicenseKey
which will fetch the license key from Lemon Squeezy. We can use this to verify that the customer has purchased the product and has available activations.activateLicenseKey
which will activate the license for a specific Github username. This will reduce the number of available activations by one.
import { ActivateLicenseResponse, LicenseKey } from '~/lib/ls/types';const BASE_ENDPOINT = `https://api.lemonsqueezy.com`;const LICENSE_KEYS_ENDPOINT = [BASE_ENDPOINT, `v1/licenses`].join(`/`);const VALIDATE_LICENSE_KEY_ENDPOINT = [LICENSE_KEYS_ENDPOINT, `validate`].join('/');const ACTIVATE_LICENSE_KEY_ENDPOINT = [LICENSE_KEYS_ENDPOINT, `activate`].join('/');export async function getLicenseKey(params: { licenseKey: string;}): Promise<LicenseKey> { const { licenseKey } = params; const response = await fetch(VALIDATE_LICENSE_KEY_ENDPOINT, { method: `POST`, headers: getHeaders(), body: new URLSearchParams({ license_key: licenseKey, }).toString(), }); if (!response.ok) { throw response; } return (await response.json()) as LicenseKey;}export async function activateLicenseKey(params: { licenseKey: string; instanceName: string;}): Promise<ActivateLicenseResponse> { const { licenseKey, instanceName } = params; const response = await fetch(ACTIVATE_LICENSE_KEY_ENDPOINT, { method: `POST`, headers: getHeaders(), body: new URLSearchParams({ license_key: licenseKey, instance_name: instanceName, }).toString(), }); if (!response.ok) { throw response; } return (await response.json()) as ActivateLicenseResponse;}function getHeaders() { return { 'Content-Type': 'application/x-www-form-urlencoded', };}
The Github API
We will use the Github API to invite the customer to the Github organization. To do so, we use the octokit
library, which is a Javascript wrapper around the Github API.
Below we write a function inviteMemberToRepository
that will invite the customer to the Github organization. It takes three parameters:
- the Github username owner of the repository
- the Github repository name
- the Github username to invite
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', });}
The Next.js API route
We will use a Next.js API route to handle the Github invitation. This is easily replicated in any other framework.
Storing the Variant ID in an environment variable
First, we need to store the variant ID of the product we want to sell in an environment variable. We can find the variant ID in the Lemon Squeezy dashboard.
We will use this variant ID to verify that the license key is valid.
LEMON_SQUEEZY_VARIANT_ID=12345
Creating the API route
Let's create an API handler at pages/api/licenses/activate.ts
that will handle the Github invitation.
async function activateLicenseHandler( req: NextApiRequest, res: NextApiResponse) { if (req.method !== `POST`) { res.status(405).json({ error: `Method not allowed` }); return; } const { licenseKey, githubUsername } = req.body; try { // validate key await validateLicenseKey(licenseKey); // invite user to github await inviteUserToRepositories(githubUsername); // activate license key const activation = await activateLicenseKey({ licenseKey, instanceName: githubUsername, }); if (activation.activated) { return res.json({ success: true, }); } return res.status(500).json({ success: false, error: `Could not activate license`, }); } catch (error) { console.error(error, `Error activating license`); return res.status((error as { status: number }).status).json(error); }}
Let's fill the blanks in the activateLicenseHandler
function.
First, we validate the license key using the validateLicenseKey
function we wrote above.
async function validateLicenseKey(licenseKey: string) { const response = await getLicenseKey({ licenseKey, }); if (!response || !response.valid) { return throwForbiddenException(`Invalid license key`); } if (response.meta.variant_id !== process.env.LEMON_SQUEEZY_VARIANT_ID) { return throwForbiddenException(`Invalid license key`); } const limit = response.license_key.activation_limit; const count = response.license_key.activation_usage; if (count >= limit) { throwForbiddenException(`License key has reached its activation limit`); }}
Then, we invite the customer to the Github organization using the inviteUserToRepositories
function we wrote above.
async function inviteUserToRepositories(username: string) { const repositories = [ // add the list of repositories you want to invite the user to ] as string[]; // add the Github username of the owner of the repositories to your .env file const owner = process.env.GITHUB_OWNER; const requests = repositories.filter(Boolean).map((repository) => { return inviteMemberToRepository({ owner, repo: repository, username, }); }); return Promise.allSettled(requests);}
Now, you can send POST requests to your /api/licenses/activate
endpoint with the following body:
{ "licenseKey": "your-license-key", "githubUsername": "your-github-username"}
Et voilà ! You can now invite your customers to your Github organization and activate their Lemon Squeezy license keys.
You can use a client-side form to fill this information and make a POST request to the API route.
One little trick to prefill the license key from your users is to use a magic interpolation in your redirect route that you will be setting up in your Lemon Squeezy dashboard.
For example, assuming you redirect your customers to https://myapp.com/licenses/activate
, you can append the following query parameter to include the license key as a query parameter:
license_key=[license_key]
For example, you can use the following redirect URL: http://localhost:3000/activate-license?action=license&license_key=[license_key]
Conclusion
In this tutorial, we saw how to use the Lemon Squeezy API to validate and activate license keys, and how to invite customers to your Github organization. This can be helpful if you're selling access to your private Github repositories and want to automate the process of inviting your customers to your Github organization.