← Back to Guides

JavaScript Browser Integration

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.

1. Quick Start

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.

2. Setup

Before writing any code, grab three things:

  1. Your API key — log into the UnifiedLicensing dashboard and head to Settings. Your vendor API key starts with ul_. Keep it handy but treat it like a password.
  2. Your product key — open the Products tab and copy the key for the product you're licensing. Each product you publish gets its own key.
  3. The SDK file — download UnifiedLicensing.JavaScript.v4.0-Full.js and copy it into your project (a /js or /vendor folder works nicely).
Security tip: Your vendor API key should never be in browser code. Only the product key (a public identifier) goes in the client. The vendor API key stays on your server. See Section 8: Server-Side Validation for the recommended pattern.

3. Initialize the Manager

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.

4. Validate a License

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;
}

5. Handle Offline Usage

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:

Why 24 hours? Web sessions are short compared to desktop apps (which typically get 72-hour grace periods). A daily check-in is a reasonable expectation for browser-based software, and it keeps revocations effective quickly.

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 });

6. Trials

Want to let people try before they buy? Two endpoints handle the whole flow.

Start a trial

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);
    }
}

Check trial status

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.

Important: The 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.

7. Heartbeat

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);
    }
}

8. Server-Side Validation Recommended

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.

Why this matters

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.

How it works

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.

Node.js / Express example

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'));

Browser code (no vendor API key!)

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.

Also proxy trials and heartbeats

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());
});

Environment variables

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.).

TL;DR: Vendor API key goes in .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.

9. Complete Example

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.

10. Troubleshooting

CORS errors in the console

"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:

"Invalid API key"

The API rejected your api_key. Double-check that:

"Quota exceeded" / HTTP 429

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:

Network errors / timeouts

"Failed to fetch" with no CORS complaint usually means the request never left the building:

Questions? Everything above is also covered in the full API reference. Happy shipping!