Add license validation to any web app in about ten minutes. No build tools, no frameworks required — just a script tag and an API key.
In a hurry? Here's the fastest possible path from zero to validated license:
<script src="UnifiedLicensing.JavaScript.v4.0-Full.js"></script>
<script>
const manager = new UnifiedLicenseManager({
vendorApiKey: 'ul_YOUR_API_KEY',
productKey: 'YOUR_PRODUCT_KEY'
});
const result = await manager.validateLicense('LICENSE-KEY');
if (result.valid) { console.log('License active!'); }
</script>
That's genuinely it. Drop in the SDK file, create a manager with your keys, call validateLicense(), and check the result. The rest of this guide walks through each piece properly so you know exactly what's happening under the hood.
Before writing any code, grab three things:
ul_. Keep it handy but treat it like a password.UnifiedLicensing.JavaScript.v4.0-Full.js and copy it into your project (a /js or /vendor folder works nicely).Create one UnifiedLicenseManager instance and reuse it across your app. Here's every option it accepts:
const manager = new UnifiedLicenseManager({
// Required
vendorApiKey: 'ul_YOUR_API_KEY', // Your vendor API key from Settings
productKey: 'YOUR_PRODUCT_KEY', // Product key from the Products tab
// Optional
baseUrl: 'https://api.unifiedlicensing.com',
// API base URL (useful for testing)
machineId: null, // Custom machine identifier; auto-generated if omitted
platform: 'web', // Platform tag sent with requests
gracePeriodHours: 24, // How long cached results stay valid offline
cacheKey: 'ul_license_cache', // localStorage key for caching
timeoutMs: 10000, // Request timeout before falling back to cache
debug: false // Log requests/responses to the console
});
You only need the two required fields to get going. The optional ones are there when you want finer control over offline behavior or debugging.
The core of everything: checking whether a license key is valid for your product. Under the hood, the SDK sends a POST request to the validation endpoint. Here's what that looks like with plain fetch, no SDK needed:
async function validateLicense(licenseKey) {
const response = await fetch('https://api.unifiedlicensing.com/api/v1/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: licenseKey,
machine_id: getOrCreateMachineId(),
platform: 'web'
})
});
const data = await response.json();
return data;
}
A successful response looks like this:
{
"valid": true,
"license_key": "LICENSE-KEY",
"status": "active",
"plan": "pro",
"expires_at": "2027-01-15T00:00:00Z",
"features": ["dark_mode", "export_pdf"],
"machine_id": "web-a1b2c3d4",
"validated_at": "2026-08-21T14:30:00Z"
}
And here's how to handle it:
const result = await validateLicense(userInput);
if (result.valid) {
console.log('Plan:', result.plan);
console.log('Expires:', result.expires_at);
console.log('Features:', result.features.join(', '));
} else {
console.log('Invalid:', result.error || 'License not recognized');
}
The machine_id field ties a license activation to a specific browser/device. If you don't send one, generate something stable — a random UUID stored in localStorage works well:
function getOrCreateMachineId() {
let id = localStorage.getItem('ul_machine_id');
if (!id) {
id = 'web-' + crypto.randomUUID().slice(0, 8);
localStorage.setItem('ul_machine_id', id);
}
return id;
}
Web apps live and die by connectivity, so the SDK caches every successful validation in localStorage. If a later validation attempt can't reach the API — flaky wifi, airplane mode, corporate firewall — the SDK falls back to the cached result instead of failing hard.
The rules are simple:
cacheKey.If you want to inspect the cache yourself:
const cached = manager.getCachedResult();
if (cached) {
console.log('Last validated:', new Date(cached.validated_at));
console.log('Cache expires:', new Date(cached.cache_expires_at));
}
// Force a fresh online check, bypassing cache:
const fresh = await manager.validateLicense(licenseKey, { forceOnline: true });
Want to let people try before they buy? Two endpoints handle the whole flow.
async function startTrial() {
const response = await fetch('https://api.unifiedlicensing.com/api/v1/start-trial', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: 'ul_YOUR_API_KEY',
product_key: 'YOUR_PRODUCT_KEY',
machine_id: getOrCreateMachineId(),
platform: 'web'
})
});
const data = await response.json();
if (data.trial_token) {
// Store the token — you'll need it for every future check
localStorage.setItem('ul_trial_token', data.trial_token);
console.log('Trial started! Expires:', data.expires_at);
} else {
console.log('Trial unavailable:', data.error);
}
}
async function checkTrial() {
const trialToken = localStorage.getItem('ul_trial_token');
if (!trialToken) return null;
const response = await fetch('https://api.unifiedlicensing.com/api/v1/check-trial', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: 'ul_YOUR_API_KEY',
product_key: 'YOUR_PRODUCT_KEY',
trial_token: trialToken
})
});
const data = await response.json();
// data.active, data.days_remaining, data.expires_at
return data;
}
The returned object tells you whether the trial is still active, how many days_remaining are left, and when it expires_at. When active flips to false, show your upgrade prompt.
trial_token is the single source of truth for that device's trial. Don't regenerate it — store it and reuse it. Starting a new trial on the same machine ID won't reset the clock.Heartbeats tell the dashboard your app is alive and being used. They're how usage analytics stay accurate, and they double as periodic re-validation. We recommend sending one weekly for web apps — often enough to be useful, rare enough to be invisible.
async function sendHeartbeat(licenseKey) {
const response = await fetch('https://api.unifiedlicensing.com/api/v1/heartbeat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: 'ul_YOUR_API_KEY',
product_key: 'YOUR_PRODUCT_KEY',
license_key: licenseKey,
machine_id: getOrCreateMachineId(),
platform: 'web'
})
});
return await response.json(); // { ok: true, next_heartbeat_due: "..." }
}
A tidy weekly pattern using timestamps in localStorage:
function shouldSendHeartbeat() {
const last = parseInt(localStorage.getItem('ul_last_heartbeat') || '0', 10);
const weekMs = 7 * 24 * 60 * 60 * 1000;
return Date.now() - last > weekMs;
}
async function maybeHeartbeat(licenseKey) {
if (!shouldSendHeartbeat()) return;
try {
await sendHeartbeat(licenseKey);
localStorage.setItem('ul_last_heartbeat', Date.now().toString());
} catch (err) {
// Never block the user because a heartbeat failed
console.warn('Heartbeat skipped:', err.message);
}
}
If you're building a web app (React, Vue, plain HTML), your vendor API key should live on your server, not in the browser. Here's why and how.
When you put the vendor API key in client-side JavaScript, anyone can open DevTools → Network tab and see it. That key lets someone make API calls as you — validate fake licenses, burn your quota, or worse.
The product key is fine in the browser — it's just a public ID that says "this is product X." But the vendor API key is your account password. Keep it server-side.
Browser Your Backend UnifiedLicensing API
│ │ │
│ 1. validateLicense(key) │ │
│ ─────────────────────────> │ │
│ │ 2. POST /validate-license │
│ │ (with vendor API key) │
│ │ ───────────────────────────> │
│ │ │
│ │ 3. { valid: true, plan: … } │
│ │ <─────────────────────────── │
│ 4. { valid: true } │ │
│ <───────────────────────── │ │
The browser only ever talks to your server. The vendor API key only travels between your server and the API. The browser never sees it.
Create a simple API route that validates licenses on behalf of the browser:
// server.js
const express = require('express');
const app = express();
app.use(express.json());
const VENDOR_API_KEY = process.env.VENDOR_API_KEY; // from .env file
const PRODUCT_KEY = process.env.PRODUCT_KEY;
app.post('/api/validate', async (req, res) => {
const { license_key, machine_id, platform } = req.body;
try {
const response = await fetch('https://api.unifiedlicensing.com/api/v1/validate-license', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: VENDOR_API_KEY,
product_key: PRODUCT_KEY,
license_key,
machine_id,
platform
})
});
const data = await response.json();
res.json(data); // Forward the result to the browser
} catch (err) {
res.status(500).json({ error: 'License server unreachable' });
}
});
app.listen(3000, () => console.log('Backend running on port 3000'));
Now your frontend calls your server, not the API directly:
async function validateLicense(licenseKey) {
const response = await fetch('/api/validate', { // Your server, not the API
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
license_key: licenseKey,
machine_id: getOrCreateMachineId(),
platform: 'web'
})
});
return await response.json();
}
const result = await validateLicense('USER-INPUT-KEY');
if (result.valid) {
console.log('License active!');
}
Notice: no vendorApiKey anywhere in the browser code. Just a call to your own server endpoint.
Same pattern — your server holds the key, the browser talks to you:
// On your server
app.post('/api/start-trial', async (req, res) => {
const { machine_id, platform } = req.body;
const response = await fetch('https://api.unifiedlicensing.com/api/v1/start-trial', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: VENDOR_API_KEY,
product_key: PRODUCT_KEY,
machine_id,
platform
})
});
res.json(await response.json());
});
app.post('/api/check-trial', async (req, res) => {
const { trial_token } = req.body;
const response = await fetch('https://api.unifiedlicensing.com/api/v1/check-trial', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: VENDOR_API_KEY,
product_key: PRODUCT_KEY,
trial_token
})
});
res.json(await response.json());
});
app.post('/api/heartbeat', async (req, res) => {
const { license_key, machine_id, platform } = req.body;
const response = await fetch('https://api.unifiedlicensing.com/api/v1/heartbeat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: VENDOR_API_KEY,
product_key: PRODUCT_KEY,
license_key,
machine_id,
platform
})
});
res.json(await response.json());
});
Never hardcode your API key. Use a .env file (and add it to .gitignore):
# .env
VENDOR_API_KEY=ul_live_YOUR_API_KEY
PRODUCT_KEY=your-product-key
Load it with dotenv in Node, or use your hosting platform's environment variable settings (Netlify, Vercel, Railway, etc.).
.env on your server. Product key goes in both server and browser. Browser calls /api/validate on your server. Server calls the API with the vendor key. Done.Here's a full, working page that ties everything together: a license input form, a validate button, a trial button, a status display, and localStorage caching. Copy it into an .html file, drop the SDK next to it, fill in your keys, and open it in a browser.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My App — Licensing</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 520px; margin: 60px auto; padding: 0 20px; }
input, button { font-size: 1rem; padding: 10px 14px; border-radius: 6px; }
input { width: 100%; box-sizing: border-box; margin-bottom: 12px; }
button { cursor: pointer; margin-right: 8px; }
#status { margin-top: 24px; padding: 16px; border-radius: 8px; display: none; }
.ok { background: #e6f4ea; color: #137333; }
.bad { background: #fce8e6; color: #c5221f; }
.info { background: #e8f0fe; color: #1967d2; }
</style>
</head>
<body>
<h1>Welcome to My App</h1>
<label for="license">License key</label>
<input id="license" placeholder="PASTE-YOUR-KEY" autocomplete="off">
<button id="btnValidate">Validate License</button>
<button id="btnTrial">Start Free Trial</button>
<div id="status"></div>
<script src="UnifiedLicensing.JavaScript.v4.0-Full.js"></script>
<script>
const manager = new UnifiedLicenseManager({
vendorApiKey: 'ul_YOUR_API_KEY',
productKey: 'YOUR_PRODUCT_KEY',
platform: 'web',
gracePeriodHours: 24,
debug: false
});
const statusBox = document.getElementById('status');
function showStatus(message, kind) {
statusBox.textContent = message;
statusBox.className = kind;
statusBox.style.display = 'block';
}
// ---- Cached license on load ----
window.addEventListener('DOMContentLoaded', () => {
const cached = manager.getCachedResult();
if (cached && cached.valid) {
showStatus(`Cached license active (${cached.plan}). Last checked ${new Date(cached.validated_at).toLocaleString()}.`, 'info');
}
});
// ---- Validate ----
document.getElementById('btnValidate').addEventListener('click', async () => {
const licenseKey = document.getElementById('license').value.trim();
if (!licenseKey) {
showStatus('Please enter a license key first.', 'bad');
return;
}
showStatus('Validating...', 'info');
try {
const result = await manager.validateLicense(licenseKey);
if (result.valid) {
// Cache for offline use
localStorage.setItem('ul_saved_key', licenseKey);
showStatus(`License active! Plan: ${result.plan}. Expires: ${result.expires_at || 'never'}.`, 'ok');
maybeHeartbeat(licenseKey);
} else {
showStatus(`Invalid license: ${result.error || 'not recognized'}`, 'bad');
}
} catch (err) {
showStatus('Network error — could not reach the license server.', 'bad');
}
});
// ---- Trial ----
document.getElementById('btnTrial').addEventListener('click', async () => {
showStatus('Starting trial...', 'info');
try {
const existingToken = localStorage.getItem('ul_trial_token');
if (existingToken) {
// Already have a token — check its status instead
const res = await fetch('https://api.unifiedlicensing.com/api/v1/check-trial', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: 'ul_YOUR_API_KEY',
product_key: 'YOUR_PRODUCT_KEY',
trial_token: existingToken
})
});
const data = await res.json();
if (data.active) {
showStatus(`Trial active — ${data.days_remaining} day(s) remaining.`, 'ok');
} else {
showStatus('Your trial has ended. Time to upgrade!', 'bad');
}
return;
}
// Fresh trial
const res = await fetch('https://api.unifiedlicensing.com/api/v1/start-trial', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: 'ul_YOUR_API_KEY',
product_key: 'YOUR_PRODUCT_KEY',
platform: 'web'
})
});
const data = await res.json();
if (data.trial_token) {
localStorage.setItem('ul_trial_token', data.trial_token);
showStatus(`Trial started! Expires ${new Date(data.expires_at).toLocaleDateString()}.`, 'ok');
} else {
showStatus(`Trial unavailable: ${data.error}`, 'bad');
}
} catch (err) {
showStatus('Network error while handling trial.', 'bad');
}
});
// ---- Weekly heartbeat ----
function shouldSendHeartbeat() {
const last = parseInt(localStorage.getItem('ul_last_heartbeat') || '0', 10);
return Date.now() - last > 7 * 24 * 60 * 60 * 1000;
}
async function maybeHeartbeat(licenseKey) {
if (!shouldSendHeartbeat()) return;
try {
await fetch('https://api.unifiedlicensing.com/api/v1/heartbeat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: 'ul_YOUR_API_KEY',
product_key: 'YOUR_PRODUCT_KEY',
license_key: licenseKey,
platform: 'web'
})
});
localStorage.setItem('ul_last_heartbeat', Date.now().toString());
} catch (err) {
console.warn('Heartbeat failed silently.');
}
}
</script>
</body>
</html>
On repeat visits, the page reads the cached result immediately and shows the saved plan without hitting the network — then quietly refreshes in the background once a week via the heartbeat.
"Access to fetch at ... has been blocked by CORS policy"
This almost always means you're testing from file:// or from a domain that isn't registered to your account. Fixes:
npx serve or python -m http.server instead of double-clicking the file.http://localhost:PORT (for dev) and your production URL.https://api.unifiedlicensing.com and not a typo'd or proxied variant.The API rejected your api_key. Double-check that:
ul_ and was copied in full (no trailing spaces or quotes).You've hit the rate limit for your plan. Common causes: validating on every page load, tight polling loops, or sharing one key across many products. Fixes:
"Failed to fetch" with no CORS complaint usually means the request never left the building:
timeoutMs in the config if you're on a slow connection.Questions? Everything above is also covered in the full API reference. Happy shipping!