An in-depth guide on how to create a downloadable application using Electron and Whop.

You can use Whop's Javascript SDK and Electron to create a downloadable application for your users. In this tutorial we will walk through how to set up a basic desktop application and use Whop for license management by creating a desktop application for a bot that buys digital bananas.
You can skip ahead to see the finished product, by cloning this quickstart GitHub repository.


Sample application:


This guide assumes you have a fully configured Whop business account and you have a license in mind to test with.

Make sure you have git and node installed before proceeding. You can find guides on how to set up these here:

This tutorial uses the following languages and technologies, so feel free to brush up on them before proceeding.

  • HTML/Javascript/CSS
  • Electron
  • Using a Terminal

Setting Up Electron

Clone the electron quickstart application with:

# Clone this repository
git clone

# Go into the repository
cd electron-quick-start

# Install dependencies
npm install

# Run the app
npm start

This should spin up a window with electron's sample app. If you are unfamiliar with Electron and would like to learn more, follow this Electron tutorial that explains the electron quick start code.

Setting up License Screen

Let's update our index.html file to have the start screen of our application. This screen will be where users need to enter their license key.

<!DOCTYPE html>
    <link href="./styles.css" rel="stylesheet" />
    <title>Banana Bot</title>
    <div class="title">
      <img class="logo" src="banana.png" />
      <h1>BANANA BOT</h1>
    <div class="license">
      <div class="licenseSubmit">
          placeholder="Enter your license key..."
        <button type="submit" id="submit">SUBMIT</button>
      <div id="error"></div>

    <script src="./renderer.js"></script>

To see your changes, you can rerun npm start in your Terminal to see the application. You can also update the app with our new code by refreshing the page with Ctrl+R.

The page now has:

  • An input field for the user's license key.
  • A submit button.
  • An empty div for rendering any errors.
  • A reference to the renderer.js script that will handle the logic for the submit button.
  • A reference to the styles.css stylesheet that will handle styling for this page.

It looks pretty ugly now, so let's also add the corresponding css file styles.css and logo asset. You can find the logo banana.png in the repo.


html {
  height: 100%;

body {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
  background-color: #48cae4;
  gap: 20px;
  color: #03045e;

.title {
  display: flex;
  flex-direction: column;
  align-items: center;

.logo {
  height: 100px;

.license {
  display: flex;
  flex-direction: column;
  gap: 16px;

.licenseSubmit {
  display: flex;
  flex-direction: row;
  gap: 12px;

select {
  border: 3px solid #0096c7;
  border-radius: 5px;
  width: 300px;
  padding: 12px;
  background-color: #caf0f8;
  -webkit-transition: 0.5s;
  transition: 0.5s;
  outline: none;
  font-size: 16px;

select:focus {
  border-color: #023e8a;

button {
  border-radius: 5px;
  background-color: #023e8a;
  color: white;
  padding: 12px 32px;
  border: none;
  -webkit-transition: 0.5s;
  transition: 0.5s;
  font-size: 16px;
  cursor: pointer;

button:hover {
  background-color: #03045e;

#error {
  color: red;

#bananaType {
  width: 100%;

Refresh the page and we should see the start of our Banana Bot!


This page doesn't do anything though, so next, we'll add some logic behind the scenes to get the app we see to communicate well with the backend processes. This will help us build up to license key verification.

Interprocess Communication

Now, we want to make sure our renderer processes that are backing our frontend app screens can communicate with our main process. Our main process will handle all of our backend logic.

At the top of preload.js add the following:

const { contextBridge, ipcRenderer } = require('electron')

// Setup interprocess communication between the javascript rendering the frontend and the main process.
contextBridge.exposeInMainWorld("api", {
    send: ( channel, data ) => ipcRenderer.invoke( channel, data ),
    handle: ( channel, callable, event, data ) => ipcRenderer.on( channel, callable( event, data ) )
} )

// Remaining code.

This allows our main processes and renderer processes to communicate with each other via the send and handle methods on the api with the key api.

With our main.js file, let's set up a listeners and add the ipcMain import from electron:

const {app, BrowserWindow, ipcMain} = require('electron')

// For handling our initial license validation and user onboarding.
ipcMain.handle('initialize', async ( event, data ) => {})

Inside of our rendered.js code that is backing our main app screen that accepts the user's license, let's add:

document.getElementById('submit').addEventListener("click", function (e) {
    // Validate license key.
    let licenseKey = document.getElementById("licenseKey").value;
    let data = {
        licenseKey: licenseKey
    api.send("initialize", data);

// If the validation failed, render an error on the UI.
api.handle('initialize', ( event, data ) => function( event, data ) {
    if (typeof data === "object" && "error" in data && data.error) {
        document.getElementById("error").innerText = data.error;
}, event);

This is sending the license key to the main process on the initialize channel and is processing the response back from the main process in case of an error.

Looks like we're good to go and can move on to hitting the Whop endpoints to validate the user-submitted licenses.

License Verification

So far, we are sending our license key to our main process, but not doing anything with it. Let's use whop to validate the license key and move the user forward to the application.

Install whopapi using npm:

npm install whopapi

Add whopapi to our main.js along with some helpful variables:

const WhopApi = require('whopapi')

var whop = new WhopApi.Whop({clientID: "<YOUR_CLIENT_ID_GOES_HERE>"});

// Here, we are maintaining some installation specific variables in memory.
// You should persist these values to a storage layer of your choice (ex. a SQL DB). 
var licenseKey = null;
var hwid = null;
var userHash = null;


Make sure to only use your clientId in your electron application. The source code will be visible so you should NOT leak your bearer token.


Make sure you persist any user information into your storage service.

Whop will help you maintain each installation of your product, and help you validate that the user's license is proper.

Let's quickly install a library that will help us determine the hardware ID a user is accessing our product from:

npm install node-machine-id

At the top of our main.js let's import:

const {machineIdSync} = require('node-machine-id')

Now, let's flesh out the three listeners we added above in our main.js file:

// Interprocess comunication to handle user inputting their license key.
ipcMain.handle('initialize', async ( event, data ) => {
  // Call Whop API to validate license key.
  try {
    var resp = await whop.validateLicenseByKey(data.licenseKey, {});
    // If valid, load application.
    if (resp.valid) {
      licenseKey = data.licenseKey;
      hwid = machineIdSync();
      userHash = Math.floor(Math.random() * 1000)

      // Update metadata for this installation.
      let _ = await whop.updateLicenseByKey(licenseKey, {
        "hwid": hwid,
        "userHash": userHash

    } else {
      // If invalid, display error.
      BrowserWindow.getFocusedWindow().webContents.send("initialize", {
        error: "Invalid license key!"
  } catch {
    // If invalid, display error.
    BrowserWindow.getFocusedWindow().webContents.send("initialize", {
      error: "Invalid license key!"

initialize accomplishes a few things here:

  • Handles messages coming from renderer.js on the license input screen.
  • Uses whop to validate the license key the user inputs.
  • Sets the hwid key on the license metadata in whop to mark that this particular license has been installed on this particular device.
  • Sets the userHash on the license metadata in whop to mark that this particular license is being used by a given user.
  • In case of an invalid license, sends an error message back to the renderer process.

Before running this, we will need to add an app.html file that the user is directed to if their license is valid.


<!DOCTYPE html>
    <link href="./styles.css" rel="stylesheet" />
    <title>Banana Bot</title>

      <select id="bananaType">
        <option value="cavendish">CAVENDISH</option>
        <option value="pisangraja">PISANG RAJA</option>
        <option value="red">RED</option>
        <option value="plantain">PLANTAIN</option>

Refresh the app with npm start or Ctrl+R. Now enter a valid license key and it should now take you to this screen:


Great! The user is in our app. Next up, we need to continuously check their key is still valid. Otherwise, we log them out.

Continual License Checks

Now a user can successfully enter into our app after providing a valid license key. But what if they cancel their payment and their license gets revoked? We need a way for our application to lock the user out if their license becomes invalid.

In a new file poller.js add the following:

// Send a ping to our main process ever 60 seconds.
// This ping will kickoff a license validation check to make
// sure the user's license is still valid (ie. not banned, revoked, etc.)
setTimeout(function() {
    api.send("validate", "ping");
}, 60 * 1000);

And include it in our app.html file at the bottom of the <body> tag:

<script src="./poller.js"></script>

The poller is sending a ping on the validate channel every 60 seconds. Our main process will need to listen for these pings and re-validate the license each time it receives a ping. Let's add a new listener in our main.js:

// This channel should be sent a message (aka triggered) periodically
// to make sure the user still has a valid license.
ipcMain.handle('validate', async (event, data) => {
  // Call Whop API to validate license key.
  try {
    var resp = await whop.validateLicenseByKey(licenseKey, {
      "hwid": machineIdSync(),
      "userHash": userHash,
  } catch {
    BrowserWindow.getFocusedWindow().webContents.send("initialize", {
      error: "License key is no longer valid!"

validate does the following couple of things:

  • Verifying that the current user information for this installation matches the stored user information.
  • Verifying that the current installation's license has not been banned, revoked, etc.

Congrats, we made a simple but fully-featured app. Next up, we'll go into a slightly more advanced topic storing Metadata on each of our users.

Advanced Topic: Metadata

Oftentimes, we want to store more data on each of the users who use our bot. We do this by storing that data in metadata. For example, we can store what type of digital bananas this user would like to buy.

Add a profile.js script:

document.getElementById('bananaType').addEventListener("change", function (e) {
    // Set profile information.
    let bananaType = document.getElementById("bananaType").value;
    let data = {
        bananaType: bananaType
    api.send("profile", data);

And include it in our app.html file:

<script src="./profile.js"></script>

In our main.js file, process the messages coming from our profile page and use whop to store this data:

ipcMain.handle('profile', async (event, data) => {
  var metadata = {}  
  // Replace relevant metadata.
  // Metadata can be used to store user session or hardware information (like a hwid or userHash)
  // or user preference information.
  metadata["bananaType"] = data.bananaType;

  // Call Whop API to set user metadata.
  // Note: this call will not clobber metadata keys that are not passed in.
  var _ = await whop.updateLicenseByKey(licenseKey, metadata)

You can store anything you'd like in metadata, so feel free to use this as you'd like!

Wrap Up

This sample desktop application walked you through how to set up an Electron app that uses Whop as its backend in order to license verification and allow a user into your app.

You can extend this app by adding additional screens specific to your app. This could include more profile pages, configuration pages, and more. Have at it and if you have any questions, don't hesitate to reach out to us on our discord.

View the full Whop Javascript SDK here:

📦Javascript SDK (Backend)