Skip to content

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.

On login / app load: POST /v1/validate { key, instanceId } → allow or deny
Per metered action: POST /v1/usage { key, amount } → allow, or 429 at the limit
On logout / shutdown: POST /v1/session/end { key, instanceId } → frees a slot at once
Keep-alive: validate again within the session TTL (default 15 minutes)
  • instanceId is any stable ID for the running session or seat: a session ID, a device ID, or userId:deviceId. Re-validating with the same instanceId refreshes 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.

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:

Terminal window
npm install ./waveform-licensing-sdk.tgz

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

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.

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
}

If the merchant set a usage limit, record consumption and respect the cap:

const u = await license.recordUsage({ key, amount: 1 }); // amount defaults to 1
if (u.ok && !u.allowed) {
// period limit reached (HTTP 429 under the hood)
return showUpgradePrompt(u.usage); // { used, limit, period }
}
// proceed with the metered action

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

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 });
  • Cache a successful validate per 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 (same instanceId) well within the session TTL to keep the seat alive during long sessions.
  • Pick the instanceId deliberately. 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.