How to Add License Verification to Your Game
How to Add License Verification to Your Game
A step-by-step guide for developers listing games on 3DIMLI to verify buyer purchases and enforce license terms using the 3DIMLI Games License Verification API.
When you sell a game on 3DIMLI, each buyer receives a unique Order Item ID. This acts as their license key. You can verify this key against 3DIMLI's API from your own license server to confirm the purchase is legitimate, determine which license tier the buyer purchased, and check whether the game is still published.
This guide walks you through the entire process, from listing your game to building a working license server.
Overview
┌──────────────┐ ┌──────────────────────┐ ┌─────────────┐
│ Game Launcher│───▶│ Your License Server │───▶│ 3DIMLI API │
│ │ │ (holds Bearer token) │ │ │
│ Player enters│ │ • Verifies key │ │ Returns: │
│ license key │ │ • Tracks devices │ │ • valid │
│ │◀───│ • Enforces limits │◀───│ • product │
└──────────────┘ └──────────────────────┘ │ name │
│ • license │
│ name │
│ • product │
│ status │
└─────────────┘
Step 1: Create Your Game Product on 3DIMLI
- Go to New Product and select Games.
- Fill in the product details (title, description, category, gameplay images, and tags).
- Set up your game license tiers. For example:
| License Tier | Description | Typical Limits | Price |
|---|---|---|---|
| Demo / Trial License | Limited build (time-locked or feature-restricted) | 1 device | Free |
| Standard License | Full game, personal use | 1–3 devices | $19 |
| Open Source License | Source available, redistribute under chosen OSI license | Unlimited | Free or pay-what-you-want |
- Upload your game as a compressed archive (ZIP, RAR, 7Z, etc.).
- Include a README in the archive with installation instructions, system requirements, and where to enter the license key.
- Submit for review.
For the full product creation guide, see Games Product Type.
Step 2: Get Your Product Slug
Your product slug is the URL path of your game on the 3DIMLI store. You'll embed this in your license server config to scope verification to your game.
Find it in your game's store URL:
https://www.3dimli.com/games/<your-game-slug>
└── product_slug ┘
For example, if your game page is at https://www.3dimli.com/games/my-game, your product slug is /my-game (the API also accepts my-game or games/my-game).
You can also copy it from the browser address bar when viewing your game page in the store.
The product slug is not a secret — it's visible in the store URL. Knowing it gives no attack surface since it's useless without a valid purchased license key.
Step 3: Understand the Verification API
3DIMLI provides a single verification endpoint, authenticated with a seller Bearer token:
POST https://www.3dimli.com/api/games/v1/verify
Authorization: Bearer <your_seller_token>
Content-Type: application/json
{
"key": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"product_slug": "/my-game"
}
| Field | Who provides it | Description |
|---|---|---|
Authorization header | Seller (held on your license server) | Your seller license-verification token from Dashboard → Settings → API Tokens |
key | Buyer (typed into launcher UI) | The license key from their purchase confirmation email |
product_slug | Seller (hardcoded in your license server config) | The game's URL slug from the store (e.g., /my-game) |
Response (valid):
{
"valid": true,
"productName": "My Game",
"licenseName": "Standard License",
"productStatus": "PUBLISHED",
"variant": { "title": "Deluxe" }
}
Response (invalid):
{
"valid": false
}
Response (rate limited):
HTTP 429 Retry-After: 45
{ "valid": false }
- A seller Bearer token is required. The endpoint is rate-limited at 30 requests/minute per IP after token authentication.
- The token is shown once when generated. Store it server-side — never embed it in your shipped game binary.
- The
productNameis the title of your game on 3DIMLI. - The
licenseNamematches whatever license tier name you set when creating the game. - The
productStatusis the lifecycle status of your game (e.g.,PUBLISHED,UNPUBLISHED). Existing buyers retain valid licenses even after unpublishing — your launcher decides whether to gate new activations. - The
variantfield is included only when the purchased game has variants enabled. - Refunded or incomplete orders return
{ "valid": false }. - The API never leaks information about why a key is invalid — it always returns the same
{ "valid": false }response. - A
401means the token is missing/invalid; a403means the token is valid but doesn't own the requested product slug or your platform subscription has lapsed.
For the full API reference, see Games License Verification API.
Step 4: Generate a Seller License-Verification Token
Verification calls require a Bearer token bound to your seller account.
- Sign in to your seller account on 3DIMLI.
- Open Dashboard → Settings → API Tokens.
- Generate a license-verification token.
- Copy the token from the one-time dialog and store it as a secret on the server you'll deploy in Step 5 (e.g., as the
DIMLI_SELLER_TOKENenvironment variable).
Anyone who downloads your game can extract embedded strings from the binary. Always proxy verification through a license server you control and keep the token there.
The same token works for every game you own. There's no need to generate a separate one per title.
Step 5: Build a License Server
3DIMLI confirms a purchase happened — it does not enforce how many machines run a game, and verification calls require your seller token, which must stay server-side. Your license server sits between your launcher and 3DIMLI: it holds the token, calls the verify endpoint, caches results, and (optionally) enforces activation limits and device binding.
Node.js Example (Express)
const express = require("express");
const app = express();
app.use(express.json());
// ─── Configuration ───────────────────────────────────────
const PRODUCT_SLUG = "/my-game"; // From Step 2
const SELLER_TOKEN = process.env.DIMLI_SELLER_TOKEN; // From Step 4 — never hardcode
// Map your license tier names to activation limits
const ACTIVATION_LIMITS = {
"Demo / Trial License": 1, // single demo device
"Standard License": 3, // up to 3 devices
"Open Source License": -1, // unlimited
};
// ─── Database (use a real database in production) ────────
const activations = new Map(); // licenseKey -> { licenseName, devices: Set }
// ─── Activate endpoint ──────────────────────────────────
app.post("/activate", async (req, res) => {
const { licenseKey, deviceId } = req.body;
if (!licenseKey || !deviceId) {
return res.status(400).json({ error: "licenseKey and deviceId required" });
}
// Check cache first (avoid unnecessary API calls)
let cached = activations.get(licenseKey);
if (!cached) {
// Verify with 3DIMLI
const response = await fetch(
"https://www.3dimli.com/api/games/v1/verify",
{
method: "POST",
headers: {
"Authorization": `Bearer ${SELLER_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ key: licenseKey, product_slug: PRODUCT_SLUG }),
}
);
const result = await response.json();
if (response.status === 401 || response.status === 403) {
console.error("3DIMLI auth failed - check token", result);
return res.status(503).json({ error: "License service unavailable" });
}
if (!result.valid) {
return res.status(403).json({ error: "Invalid license key" });
}
// Optional: refuse new activations when the game is unpublished.
// Existing players keep playing offline.
if (result.productStatus === "UNPUBLISHED") {
console.warn(`Activation attempt while UNPUBLISHED for ${licenseKey}`);
// Decide your policy. Default: still allow.
}
cached = { licenseName: result.licenseName, devices: new Set() };
activations.set(licenseKey, cached);
}
// Check activation limit
const limit = ACTIVATION_LIMITS[cached.licenseName] ?? 1;
if (cached.devices.has(deviceId)) {
return res.json({
activated: true,
licenseName: cached.licenseName,
});
}
if (limit !== -1 && cached.devices.size >= limit) {
return res.status(403).json({
error: `Activation limit reached (${limit}). Deactivate a device first.`,
currentDevices: cached.devices.size,
maxDevices: limit,
});
}
// Register device
cached.devices.add(deviceId);
res.json({ activated: true, licenseName: cached.licenseName });
});
// ─── Deactivate endpoint ─────────────────────────────────
app.post("/deactivate", (req, res) => {
const { licenseKey, deviceId } = req.body;
const cached = activations.get(licenseKey);
if (cached) {
cached.devices.delete(deviceId);
}
res.json({ deactivated: true });
});
// ─── Check status endpoint ──────────────────────────────
app.post("/status", (req, res) => {
const { licenseKey } = req.body;
const cached = activations.get(licenseKey);
if (!cached) {
return res.json({ activated: false });
}
const limit = ACTIVATION_LIMITS[cached.licenseName] ?? 1;
res.json({
activated: true,
licenseName: cached.licenseName,
devicesUsed: cached.devices.size,
maxDevices: limit === -1 ? "unlimited" : limit,
});
});
app.listen(3000, () => console.log("Game license server running on port 3000"));
Python Example (Flask)
import os
from flask import Flask, request, jsonify
import requests
app = Flask(__name__)
PRODUCT_SLUG = "/my-game" # From Step 2
SELLER_TOKEN = os.environ["DIMLI_SELLER_TOKEN"] # From Step 4 — never hardcode
ACTIVATION_LIMITS = {
"Demo / Trial License": 1,
"Standard License": 3,
"Open Source License": -1, # unlimited
}
activations = {} # license_key -> { "licenseName": str, "devices": set }
@app.route("/activate", methods=["POST"])
def activate():
data = request.json
license_key = data.get("licenseKey")
device_id = data.get("deviceId")
if not license_key or not device_id:
return jsonify({"error": "licenseKey and deviceId required"}), 400
cached = activations.get(license_key)
if not cached:
# Verify with 3DIMLI
resp = requests.post(
"https://www.3dimli.com/api/games/v1/verify",
headers={"Authorization": f"Bearer {SELLER_TOKEN}"},
json={"key": license_key, "product_slug": PRODUCT_SLUG},
)
result = resp.json()
if resp.status_code in (401, 403):
return jsonify({"error": "License service unavailable"}), 503
if not result.get("valid"):
return jsonify({"error": "Invalid license key"}), 403
cached = {"licenseName": result.get("licenseName"), "devices": set()}
activations[license_key] = cached
limit = ACTIVATION_LIMITS.get(cached["licenseName"], 1)
if device_id in cached["devices"]:
return jsonify({"activated": True, "licenseName": cached["licenseName"]})
if limit != -1 and len(cached["devices"]) >= limit:
return jsonify({"error": f"Activation limit reached ({limit})"}), 403
cached["devices"].add(device_id)
return jsonify({"activated": True, "licenseName": cached["licenseName"]})
@app.route("/deactivate", methods=["POST"])
def deactivate():
data = request.json
cached = activations.get(data.get("licenseKey"))
if cached:
cached["devices"].discard(data.get("deviceId"))
return jsonify({"deactivated": True})
if __name__ == "__main__":
app.run(port=3000)
Step 6: Add License Check to Your Launcher
Your launcher calls your license server (not 3DIMLI directly), since the seller token must stay server-side.
Desktop Launcher (JavaScript / Electron / Tauri)
const os = require("os");
const crypto = require("crypto");
// Generate a stable device ID from hardware info
function getDeviceId() {
const raw = `${os.hostname()}-${os.platform()}-${os.cpus()[0]?.model}`;
return crypto.createHash("sha256").update(raw).digest("hex").slice(0, 32);
}
async function activate(licenseKey) {
const deviceId = getDeviceId();
const response = await fetch("https://your-license-server.com/activate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ licenseKey, deviceId }),
});
const result = await response.json();
if (result.activated) {
// Save license key locally so player doesn't re-enter it
saveToLocalStorage("licenseKey", licenseKey);
console.log(`Activated: ${result.licenseName}`);
return { success: true, licenseName: result.licenseName };
} else {
console.error(`Activation failed: ${result.error}`);
return { success: false, error: result.error };
}
}
// On launcher startup
async function onLauncherStart() {
const savedKey = readFromLocalStorage("licenseKey");
if (savedKey) {
const result = await activate(savedKey);
if (result.success) {
unlockGame(result.licenseName);
return;
}
}
// Show license key input dialog
showActivationPrompt();
}
Unity (C#)
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
public class LicenseClient
{
private static readonly HttpClient http = new HttpClient();
private const string ServerUrl = "https://your-license-server.com/activate";
public static async Task<(bool ok, string licenseName, string error)> Activate(
string licenseKey,
string deviceId)
{
var payload = JsonSerializer.Serialize(new { licenseKey, deviceId });
var content = new StringContent(payload, Encoding.UTF8, "application/json");
var response = await http.PostAsync(ServerUrl, content);
var body = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<ActivateResponse>(body);
if (result.activated)
return (true, result.licenseName, null);
return (false, null, result.error);
}
private class ActivateResponse
{
public bool activated { get; set; }
public string licenseName { get; set; }
public string error { get; set; }
}
}
Web / Browser Game
async function checkLicense(licenseKey) {
const deviceId = localStorage.getItem("deviceId") || crypto.randomUUID();
localStorage.setItem("deviceId", deviceId);
const res = await fetch("https://your-license-server.com/activate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ licenseKey, deviceId }),
});
return res.json();
}
Step 7: Handle Edge Cases
Re-verification
Don't call the 3DIMLI API on every launcher start. Instead:
- Cache the verification result on your server with a timestamp.
- Re-verify periodically (e.g., every 24–48 hours) to catch refunds.
- Allow offline grace periods. If your server is unreachable, let the player keep playing for a set time (e.g., 7 days). Single-player titles should lean toward generous offline windows.
// Example: Cache with expiry
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
async function verifyWithCache(licenseKey) {
const cached = activations.get(licenseKey);
if (cached && Date.now() - cached.verifiedAt < CACHE_DURATION) {
return cached; // Use cached result
}
// Re-verify with 3DIMLI
const response = await fetch("https://www.3dimli.com/api/games/v1/verify", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.DIMLI_SELLER_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ key: licenseKey, product_slug: PRODUCT_SLUG }),
});
const result = await response.json();
if (!result.valid) {
activations.delete(licenseKey); // Revoke (e.g., refunded)
return null;
}
// Update cache
if (cached) {
cached.verifiedAt = Date.now();
}
return cached;
}
Handling Refunds
When a buyer gets a refund, 3DIMLI's API will return { "valid": false } for their key. On your next re-verification cycle, revoke access gracefully:
- Show a message: "Your license is no longer valid. Please contact support."
- Don't delete save files or progress, just lock the launch button.
Handling Unpublished Games
If you unpublish your game on 3DIMLI, existing buyers still hold valid licenses — valid stays true but productStatus becomes UNPUBLISHED. Most games let existing players keep playing and only refuse new device activations in that case.
Step 8: Tell Your Buyers Where to Find Their Key
In your game's README and activation dialog, tell buyers:
Your license key is your Order Item ID. You can find it in:
- Your purchase confirmation email from 3DIMLI.
- Your 3DIMLI dashboard → Orders → expand the order → License Product ID.
- Your 3DIMLI dashboard → Downloads → click the license details → Item ID.
Test Activation Script
Use this script to test your integration end-to-end. Replace the values in CONFIG with your own product slug, a valid license key from a real purchase, and your seller token.
/**
* 3DIMLI Games Activation — Test Script
*
* Run: node test-activation-games.mjs
*
* Replace the values in CONFIG with your own before running.
*/
// ─── CONFIG ──────────────────────────────────────────────────────────────────
const CONFIG = {
baseUrl: "https://www.3dimli.com",
// Your product slug — the path segment after "games/" in the store URL.
// Example: if your game is at 3dimli.com/games/my-game, the slug is "/my-game"
productSlug: "YOUR_PRODUCT_SLUG_HERE",
// A license key to test — paste the order item ID from a real completed purchase.
testKey: "YOUR_LICENSE_KEY_HERE",
// Your seller license-verification token — generate at:
// https://www.3dimli.com/dashboard/seller/settings/api-tokens
// Prefer reading from process.env.DIMLI_SELLER_TOKEN over hardcoding.
sellerToken: process.env.DIMLI_SELLER_TOKEN ?? "YOUR_SELLER_TOKEN_HERE",
};
// ─────────────────────────────────────────────────────────────────────────────
async function verifyLicenseKey(key, productSlug) {
const url = `${CONFIG.baseUrl}/api/games/v1/verify`;
let response;
try {
response = await fetch(url, {
method: "POST",
headers: {
"Authorization": `Bearer ${CONFIG.sellerToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ key, product_slug: productSlug }),
});
} catch (networkError) {
return { valid: false, error: "Network error — check your internet connection." };
}
if (response.status === 401) {
return { valid: false, error: "Unauthorized — token missing, invalid, or revoked." };
}
if (response.status === 403) {
return { valid: false, error: "Forbidden — token doesn't own this game or your subscription has lapsed." };
}
if (response.status === 429) {
const retryAfter = response.headers.get("Retry-After") ?? "60";
return { valid: false, error: `Rate limit reached. Retry after ${retryAfter} seconds.` };
}
if (!response.ok) {
return { valid: false, error: `Server error (HTTP ${response.status}).` };
}
return await response.json();
}
// ─── TEST CASES ──────────────────────────────────────────────────────────────
async function runTests() {
console.log("3DIMLI Games Activation Test");
console.log("=============================\n");
// Test 1: Valid key from CONFIG
console.log("Test 1 — Valid key (from CONFIG)");
console.log(` key: ${CONFIG.testKey}`);
console.log(` product_slug: ${CONFIG.productSlug}`);
const result1 = await verifyLicenseKey(CONFIG.testKey, CONFIG.productSlug);
if (result1.valid) {
console.log(` ✓ VALID — Game: "${result1.productName}", License: "${result1.licenseName ?? "Standard"}", Status: ${result1.productStatus}`);
if (result1.productStatus === "UNPUBLISHED") {
console.warn(" WARN — Game is UNPUBLISHED. Buyers still hold valid licenses; decide your launcher policy.");
}
} else {
console.log(` ✗ INVALID${result1.error ? " — " + result1.error : ""}`);
console.log(" (Replace CONFIG values with your real product slug and a valid license key)");
}
console.log();
// Test 2: Malformed key (not a UUID)
console.log("Test 2 — Malformed key (not a UUID)");
console.log(" key: not-a-valid-uuid");
const result2 = await verifyLicenseKey("not-a-valid-uuid", CONFIG.productSlug);
console.log(result2.valid ? " ✗ valid (unexpected)" : " ✓ correctly rejected as invalid");
console.log();
// Test 3: Random UUID (unknown key)
const randomUuid = "00000000-0000-0000-0000-000000000000";
console.log("Test 3 — Unknown UUID key");
console.log(` key: ${randomUuid}`);
const result3 = await verifyLicenseKey(randomUuid, CONFIG.productSlug);
console.log(result3.valid ? " ✗ returned valid (unexpected)" : " ✓ correctly returned { valid: false }");
console.log();
// Test 4: Valid key against wrong product slug (cross-game scope check)
const wrongSlug = "/wrong-game-slug";
console.log("Test 4 — Valid key used against wrong product_slug (scope check)");
console.log(` key: ${CONFIG.testKey}`);
console.log(` product_slug: ${wrongSlug} (wrong game)`);
const result4 = await verifyLicenseKey(CONFIG.testKey, wrongSlug);
console.log(
result4.valid
? " ✗ returned valid — scope check failed (report this as a bug)"
: " ✓ correctly rejected — key is scoped to its original game only"
);
console.log("\nDone.");
}
runTests().catch((err) => {
console.error("Unexpected error:", err);
process.exit(1);
});
Complete Checklist
- Game product created on 3DIMLI with license tiers defined
- Product slug identified from your store URL
- Seller license-verification token generated and stored as a server-side secret
- License server deployed; launcher calls your server instead of 3DIMLI directly
- Activation / device limits enforced if required
- Launcher prompts for license key on first launch
- Verification results cached on the license server to reduce API calls
- Re-verification runs periodically (every 24-48h)
- Offline grace period implemented (especially for single-player titles)
- Refund handling added (revoke on re-verify failure, preserve save data)
-
productStatus: UNPUBLISHEDpolicy chosen (existing players keep playing vs. block new activations) - README includes instructions on where buyers find their license key
- Tested the full flow: purchase → enter key → activate → play
Related
- Games Product Type. Creating and listing your game on 3DIMLI
- Games License Verification API. Full API reference
- Software License Integration Guide. Equivalent walkthrough for software products
- Upload Your First Product. General product upload guide