You spent months, maybe years, building your game. Someone buys it, shares the download link, and suddenly 50 people are playing for free. Without licensing, you have no way to control who uses your software.
For indie developers, licensing isn't about being greedy. It's about:
Game licensing is a system where players receive a unique key when they purchase your game, and your game validates that key before running. Here's the flow:
ABCD-1234-EFGH-5678)These terms get confused a lot. Here's the difference:
| Approach | What It Does | Pros | Cons |
|---|---|---|---|
| Licensing | Validates a key before running | Simple, works offline with caching | Keys can be shared |
| DRM | Always-online verification (e.g., Denuvo) | Harder to crack | Annoys legit users, expensive |
| Copy Protection | Obfuscates code, encrypts assets | Slows casual piracy | Determined pirates always win |
For indie devs, licensing is the sweet spot. It's lightweight, works offline, and doesn't punish your real customers. Pair it with hardware fingerprinting and you've got solid protection.
You can build your own system or use a service like UnifiedLicensing. For most indie devs, a service is faster and more secure.
Sign up, create a product, and get your API key and product key. On UnifiedLicensing, this takes about 2 minutes.
You generate keys for your customers. This can be:
Add the licensing check to your game. The SDK handles validation, caching, and error handling.
When a player starts your game, check their license key. If it's valid, let them play. If not, show the activation screen.
Players don't always have internet. Smart caching means a previously-validated key keeps working for days, even offline.
Here's a complete license validation script for Unity:
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
public class LicenseManager : MonoBehaviour
{
private const string API_URL = "https://api.unifiedlicensing.com/api/v1/validate-license";
private const string API_KEY = "ul_YOUR_API_KEY";
private const string PRODUCT_KEY = "YOUR_PRODUCT_KEY";
private string licenseKey;
private bool isLicensed = false;
void Start()
{
licenseKey = PlayerPrefs.GetString("LicenseKey", "");
if (!string.IsNullOrEmpty(licenseKey))
{
StartCoroutine(ValidateLicense(licenseKey));
}
else
{
ShowActivationScreen();
}
}
IEnumerator ValidateLicense(string key)
{
string json = JsonUtility.ToJson(new LicenseRequest
{
api_key = API_KEY,
product_key = PRODUCT_KEY,
license_key = key,
machine_id = SystemInfo.deviceUniqueIdentifier,
platform = "desktop"
});
using (UnityWebRequest req = new UnityWebRequest(API_URL, "POST"))
{
req.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(json));
req.downloadHandler = new DownloadHandlerBuffer();
req.SetRequestHeader("Content-Type", "application/json");
yield return req.SendWebRequest();
if (req.result == UnityWebRequest.Result.Success)
{
LicenseResponse response = JsonUtility.FromJson<LicenseResponse>(req.downloadHandler.text);
if (response.valid)
{
isLicensed = true;
OnLicensed();
}
else
{
ShowError(response.error);
}
}
else
{
// Offline? Check cache
if (PlayerPrefs.GetInt("LastValid", 0) == 1)
{
isLicensed = true;
OnLicensed();
}
else
{
ShowError("Network error. Please check your connection.");
}
}
}
}
void OnLicensed()
{
PlayerPrefs.SetString("LicenseKey", licenseKey);
PlayerPrefs.SetInt("LastValid", 1);
Debug.Log("License validated! Unlocking game...");
// Unlock your game here
}
void ShowActivationScreen()
{
Debug.Log("Show license input UI");
}
void ShowError(string msg)
{
Debug.LogError("License error: " + msg);
}
[System.Serializable]
class LicenseRequest
{
public string api_key;
public string product_key;
public string license_key;
public string machine_id;
public string platform;
}
[System.Serializable]
class LicenseResponse
{
public bool valid;
public string error;
public string plan;
public string expires_at;
}
}
extends Node
const API_URL = "https://api.unifiedlicensing.com/api/v1/validate-license"
const API_KEY = "ul_YOUR_API_KEY"
const PRODUCT_KEY = "YOUR_PRODUCT_KEY"
var is_licensed := false
func _ready():
var saved_key = _load_key()
if saved_key != "":
_validate(saved_key)
else:
_show_activation()
func _validate(key: String):
var http = HTTPRequest.new()
add_child(http)
http.request_completed.connect(_on_validation_complete)
var body = JSON.stringify({
"api_key": API_KEY,
"product_key": PRODUCT_KEY,
"license_key": key,
"machine_id": OS.get_unique_id(),
"platform": "desktop"
})
http.request(API_URL,
["Content-Type: application/json"],
HTTPClient.METHOD_POST,
body)
func _on_validation_complete(result, response_code, headers, body):
if result == OK:
var json = JSON.parse_string(body.get_string_from_utf8())
if json and json.get("valid", false):
is_licensed = true
_on_licensed()
else:
_show_error(json.get("error", "Invalid key"))
else:
# Offline fallback
if FileAccess.file_exists("user://license_valid.tmp"):
is_licensed = true
_on_licensed()
else:
_show_error("Network error")
func _on_licensed():
var f = FileAccess.open("user://license_valid.tmp", FileAccess.WRITE)
f.close()
print("License validated! Starting game...")
func _show_activation():
print("Show license input")
func _show_error(msg: String):
print("Error: ", msg)
func _load_key() -> String:
if FileAccess.file_exists("user://license.key"):
var f = FileAccess.open("user://license.key", FileAccess.READ)
return f.get_as_text().strip_edges()
return ""
// LicenseManager.h
#pragma once
#include "CoreMinimal.h"
#include "Http.h"
UCLASS()
class ULicenseManager : public UObject
{
GENERATED_BODY()
public:
void ValidateLicense(const FString& Key);
DECLARE_DELEGATE_OneParam(FOnValidation, bool);
FOnValidation OnValidation;
};
// LicenseManager.cpp
#include "LicenseManager.h"
#include "Interfaces/IHttpRequest.h"
#include "Interfaces/IHttpResponse.h"
void ULicenseManager::ValidateLicense(const FString& Key)
{
TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
Request->SetURL("https://api.unifiedlicensing.com/api/v1/validate-license");
Request->SetVerb("POST");
Request->SetHeader("Content-Type", "application/json");
FString Json = FString::Printf(TEXT(
"{\"api_key\":\"ul_YOUR_API_KEY\",\"product_key\":\"YOUR_PRODUCT_KEY\","
"\"license_key\":\"%s\",\"machine_id\":\"%s\",\"platform\":\"desktop\"}"
), *Key, *FGenericPlatformMisc::GetDeviceId());
Request->SetContentAsString(Json);
Request->OnProcessRequestComplete().BindLambda(
[this](FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bSuccess)
{
if (bSuccess && Resp.IsValid())
{
// Parse response and call OnValidation
}
});
Request->ProcessRequest();
}
Web games are the simplest to license. Just add a validation call when the game loads:
const API = 'https://api.unifiedlicensing.com/api/v1';
async function checkLicense() {
const key = localStorage.getItem('game_license');
if (!key) return showActivation();
const res = await fetch(`${API}/validate-license`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: 'ul_YOUR_API_KEY',
product_key: 'YOUR_PRODUCT_KEY',
license_key: key,
machine_id: navigator.userAgent,
platform: 'web'
})
});
const data = await res.json();
if (data.valid) {
startGame();
} else {
localStorage.removeItem('game_license');
showActivation();
}
}
function activate() {
const key = document.getElementById('license-input').value;
localStorage.setItem('game_license', key);
checkLicense();
}
checkLicense();
| Model | How Licensing Helps | Example |
|---|---|---|
| One-time purchase | Permanent key, machine-locked | Steam game, desktop app |
| Subscription | Monthly key, expires when sub ends | SaaS tool, cloud service |
| Free trial | Time-limited key (7/14/30 days) | "Try free for 14 days" |
| Freemium | Basic key free, premium key paid | Free tier + Pro features |
| Pay-what-you-want | Any purchase generates a key | itch.io model |
UnifiedLicensing gives you license keys, SDK integration for every engine, and offline support — all on a free tier.
Get Started FreeUnifiedLicensing offers a free tier for indie developers. Paid plans start at $9/month for more keys and analytics. Most indie games never outgrow the free tier.
Yes. Steam has its own licensing, but you can add an extra layer with UnifiedLicensing for off-platform sales, direct sales, or beta testing.
Not if you do it right. One key entry on first launch, then automatic silent validation in the background. Most players never think about it.
Smart caching means previously-validated keys keep working for 7+ days offline. Your players won't notice a brief outage.
Yes. Generate 100, 500, or 1000 keys at once from the dashboard or via the API. Perfect for press kits and giveaways.