Online integration
For products that are always connected. Your backend validates the license in real time, so revocation is instant and you can enforce concurrent sessions and usage limits. There are no signed files and no client-side cryptography.
The flow
Section titled “The flow”On login / app load: POST /v1/validate { key, instanceId } → allow or denyPer metered action: POST /v1/usage { key, amount } → allow, or 429 at the limitOn logout / shutdown: POST /v1/session/end { key, instanceId } → frees a slot at onceKeep-alive: validate again within the session TTL (default 15 minutes)instanceIdis any stable ID for the running session or seat: a session ID, a device ID, oruserId:deviceId. Re-validating with the sameinstanceIdrefreshes the session and does not consume a new slot.- Sessions that are not re-validated within the product’s session TTL are pruned automatically, so a crashed client frees its slot on its own.
Validate
Section titled “Validate”The TypeScript SDK is delivered as a package file, not through the public npm registry. Download it from theIntegration page of the Waveform app in your store admin (or from your Waveform account), then install it from disk:
npm install ./waveform-licensing-sdk.tgzPrefer not to install anything? Every example below also works as plain HTTP.
import { createClient } from "@waveform/licensing";
const license = createClient({ apiBaseUrl: "https://api.waveformanager.com" });
const result = await license.validate({ key: user.licenseKey, instanceId: session.id, // unique per active session label: `${user.email} · ${ua.browser}`, // optional, shown in dashboards});
if (result.ok) { // result.features, result.expiresAt, result.sessions {used,max}, result.usage {used,limit,period} grantAccess(result.features);} else if (result.error === "session-limit") { // result.sessions lists the active sessions; result.max is the cap showTooManySessions(result.sessions, result.max);} else { // "invalid-key" | "revoked" | "expired" | "network" denyAccess(result.error);}curl -sX POST https://api.waveformanager.com/v1/validate \ -H 'content-type: application/json' \ -d '{"key":"ACME-7F3K-9QPD-2MXR","instanceId":"sess_123"}'# 200 → { ok, status, mode, features, expiresAt, sessions:{used,max}, usage:{used,limit,period} }# 409 → { error:"session-limit", sessions:[...], max }# 403 → { error:"revoked" | "expired" } 404 → { error:"invalid-key" }import requests
r = requests.post("https://api.waveformanager.com/v1/validate", json={"key": key, "instanceId": session_id}, timeout=20)data = r.json()if r.status_code == 200: grant(data["features"])elif r.status_code == 409: too_many(data["sessions"], data["max"])else: deny(data.get("error"))Decide your failure mode
Section titled “Decide your failure mode”When the API is unreachable (error === "network" in the SDK, or a connection error over plain HTTP) you must decide
deliberately. Fail open (grant temporarily) for a good customer experience, or fail closed (deny) for strict
enforcement. A common middle ground is to cache the last successful validate for a few minutes and only hard-deny
after repeated failures.
Keep an outage cheap. The SDK’s defaults retry three times (waiting 1 s, then 2 s) with a 20 s timeout per attempt.
That suits a one-off login check but not a check on every request: while the API is unreachable, each request would wait
about three seconds before failing open, and longer if the connection hangs. For a check in your request path, create the
client with maxAttempts: 1 and a short timeoutMs (3000 is a good start) so a licensing outage costs a few
milliseconds, not seconds. If you cache validate results, a revocation then takes effect only when the cached entry
expires.
Express middleware
Section titled “Express middleware”import express from "express";import { createClient } from "@waveform/licensing";
// On the request path: one attempt and a short timeout, so an outage fails open quickly.const license = createClient({ apiBaseUrl: process.env.WAVEFORM_API_URL!, maxAttempts: 1, timeoutMs: 3000,});
export function requireLicense(req, res, next) { license .validate({ key: req.user.licenseKey, instanceId: req.sessionID }) .then((r) => { if (r.ok) { req.licenseFeatures = r.features; return next(); } if (r.error === "session-limit") return res.status(409).json({ error: "too_many_sessions", sessions: r.sessions }); if (r.error === "network") return next(); // fail open; return 503 here to fail closed return res.status(403).json({ error: r.error }); }) .catch(() => next()); // never crash the request on a licensing hiccup}Usage metering
Section titled “Usage metering”If the merchant set a usage limit, record consumption and respect the cap:
const u = await license.recordUsage({ key, amount: 1 }); // amount defaults to 1if (u.ok && !u.allowed) { // period limit reached (HTTP 429 under the hood) return showUpgradePrompt(u.usage); // { used, limit, period }}// proceed with the metered actionUsage buckets reset by the configured period (month, day, or total). The current counter is also returned by
validate, so you can show “423 / 1,000 this month” without an extra call. When a request would exceed the limit the
counter is not incremented, and the response is 429 with allowed: false.
End a session
Section titled “End a session”Free a concurrency slot the moment a user logs out; otherwise it frees itself after the session TTL:
await license.endSession({ key, instanceId: session.id });Production tips
Section titled “Production tips”- Cache a successful
validateper session for 30 to 60 seconds so you do not validate on every request. Always validate fresh on login. - Mind the rate limit. The public API allows 60 requests per minute per client IP. A backend that validates on behalf of all its users shares one IP, so caching is not optional at any real volume. See Rate limits.
- Heartbeat by re-calling
validate(sameinstanceId) well within the session TTL to keep the seat alive during long sessions. - Pick the
instanceIddeliberately. Per browser session means concurrent logins count; per device means concurrent devices; per user is effectively single-session. - Retry sensibly. The SDK retries transient, 5xx, and cold-start failures automatically (
maxAttempts,timeoutMs). Defaults are generous for a login; lower them for per-request checks.
See the API reference for the full schema of every field and error.