API reference
The public endpoints your software calls. All are POST unless noted, accept and return JSON, and live under /v1.
| Setting | Value |
|---|---|
| Base URL | https://api.waveformanager.com |
| Auth | None. These are client-facing endpoints; the activation key is the credential. Never send any other secret. |
| Transport | HTTPS only. The SDKs refuse to send a key over cleartext (loopback excepted). |
| Content type | application/json on requests and responses. Request bodies are limited to 32 KB. |
| Errors | Non-2xx responses carry { "error": "<code>" }. Codes are listed per endpoint and collected on Errors. |
| Rate limit | Per client IP; see Rate limits and reliability. |
Ignore response fields you do not recognize: responses can gain optional fields over time.
Two groups of endpoints: offline (signed-token issuance) and online (real-time enforcement). Which applies depends on the product’s enforcement mode.
Offline endpoints
Section titled “Offline endpoints”POST /v1/activate
Section titled “POST /v1/activate”Activate a key on a device and receive a signed license token.
Request
{ "key": "ACME-7F3K-9QPD-2MXR", "fingerprint": "<base64url HMAC>", "deviceName": "Studio Mac", "productId": "<Product.id>"}key(required): The activation key.fingerprint(required): This machine’s fingerprint.deviceName(required): A human-readable name, 1 to 80 characters (longer is400 bad-request); shown in device managers.productId(recommended): The product’s internalProduct.id, never a store SKU,woo:mapping ID, or Shopify GID. When sent, a key for a different product is refused (403 product-mismatch). Once the merchant turns on strict product binding, omitting it is refused too (403 product-id-required). Also accepted on/v1/heartbeat.priorToken(optional): The token this machine held under its previous fingerprint. Send it alongside a new fingerprint when a stored token fails withfingerprint-mismatch(hardware change). On a match the old device row is replaced by the new one in the same step, so the client never seesseat-limitfor its own drifted machine, and the response carriesrebound: true. The new fingerprint is still counted as a new machine for the distinct-machine budget, so a re-bind can return409 activation-limit. ApriorTokenthat fails verification, belongs to another license, or matches no device is silently ignored and activation continues as a normal new-device request.
200 OK
{ "token": "<payload>.<signature>", "publicKey": "<raw base64url Ed25519 public key>", "device": { "id": "…", "name": "Studio Mac", "createdAt": 1700000000, "lastSeenAt": 1700000000 }, "reactivated": false, "rebound": false}reactivated: true: this fingerprint was already activated, so no new seat was used.rebound: true: apriorTokenre-bind succeeded.
Errors
| Status | error |
Meaning |
|---|---|---|
| 409 | seat-limit |
Device cap reached. The body includes devices: DeviceInfo[] so you can offer a swap. |
| 409 | activation-limit |
The distinct-machine budget for the window is exhausted. Body: distinctMachines, windowDays. Freeing a seat will not clear this: say “contact support”, do not offer a swap. |
| 404 | invalid-key |
No such key. |
| 403 | revoked / expired |
The license was revoked, or is past its grace window. |
| 403 | blocked |
The license’s customer is on the merchant’s block list. Treat like revoked and drop the stored license. |
| 403 | product-mismatch / product-id-required |
Wrong productId for this key, or none sent where required. |
| 403 | product-not-found |
The productId does not exist. |
| 403 | merchant-inactive |
The merchant’s Waveform subscription is not active. New devices are refused; devices already registered can still re-activate. |
| 400 | bad-request |
Missing or invalid key, fingerprint, or deviceName. |
POST /v1/demo
Section titled “POST /v1/demo”Mint (or resume) a time-limited demo for a product, bound to one device. No key is needed.
Request
{ "productId": "<Product.id>", "fingerprint": "<base64url HMAC>", "deviceName": "Studio Mac" }200 OK
{ "token": "<payload>.<signature>", "publicKey": "<raw base64url>", "expiresAt": 1781410876, "reissued": false }Errors
| Status | error |
Meaning |
|---|---|---|
| 404 | product-not-found |
Unknown product. |
| 403 | revoked |
An operator revoked this machine’s demo. The client must drop its cached verdict and not retry. |
| 403 | demo-already-used |
This machine’s demo has simply run out. |
| 403 | blocked |
The fingerprint was recorded by a customer block. A demo request carries no customer identity, so only a previously seen fingerprint is caught. |
| 403 | merchant-inactive |
The merchant’s subscription is not active. |
| 400 | bad-request |
Missing productId, fingerprint, or deviceName. |
Demos have a tighter per-IP rate limit than other endpoints, to bound trial abuse.
POST /v1/heartbeat
Section titled “POST /v1/heartbeat”Refresh the signed token for an already-activated device. This renews the offline lease (no new seat) and is how revocation reaches machines in the field.
Request { "key": "…", "fingerprint": "…", "productId": "…" }
200 OK { "token": "…", "publicKey": "…", "device": { … } }
| Status | error |
Meaning |
|---|---|---|
| 404 | invalid-key / not-activated |
No such key, or this device never activated it. |
| 403 | revoked / expired / blocked |
A definitive verdict. Drop the stored token. |
| 403 | product-mismatch / product-id-required / product-not-found |
See /v1/activate. |
| 400 | bad-request |
Missing key or fingerprint. |
POST /v1/devices
Section titled “POST /v1/devices”List the devices bound to a key, for a “manage devices” screen. Fingerprints are never returned.
Request { "key": "…", "fingerprint": "…" }. fingerprint is optional; when sent, the row whose stored fingerprint
matches is marked current: true.
200 OK
{ "devices": [ { "id": "…", "name": "Studio Mac", "createdAt": 1700000000, "lastSeenAt": 1700000000, "current": true, "worksUntil": 1702592000 } ] }worksUntil (unix seconds) is how long that device keeps working offline: the end of the last lease signed for it plus the
product’s grace. It is absent on rows that predate lease tracking. The seat-limit device list on /v1/activate carries it
too. Errors: 404 invalid-key.
POST /v1/deactivate
Section titled “POST /v1/deactivate”Free a seat, either the caller’s own (fingerprint) or a specific device from the device list (deviceId).
Request { "key": "…", "fingerprint": "…" } or { "key": "…", "deviceId": "…" }
200 OK { "removed": true }
| Status | error |
Meaning |
|---|---|---|
| 404 | invalid-key |
No such key. |
| 400 | bad-request |
Neither fingerprint nor deviceId was given. |
| 409 | support-limit |
{ key, deviceId } only, and only on a deactivate-policy product: the license has used its remote-release budget for the rolling window. Body: used, limit, windowDays. The { key, fingerprint } self-release path is never budgeted. |
See Seat policies.
POST /v1/check
Section titled “POST /v1/check”Confirm a key is valid without registering a device or claiming a seat. Use it to validate a typed key before asking the customer to name their machine.
Request { "key": "…" }
200 OK { "ok": true }
Errors: 404 invalid-key, 403 revoked / expired, 400 bad-request.
GET /v1/products/{productId}/pubkey
Section titled “GET /v1/products/{productId}/pubkey”Public and read-only. The merchant’s Ed25519 public keys for a product, by internal Product.id. Used by the
build-time key check and to confirm the key you embed.
200 OK
{ "productId": "…", "publicKeyRawB64": "<active key>", "publicKeys": ["<active>", "<keys still verifying live leases, during a rotation>"] }404 { "error": "not-found" } for an unknown ID, including a store mapping ID. See
Signing keys and rotation.
Online endpoints
Section titled “Online endpoints”POST /v1/validate
Section titled “POST /v1/validate”Validate a license in real time and register or refresh a concurrent session.
Request
{ "key": "ACME-7F3K-9QPD-2MXR", "instanceId": "sess_123", "label": "alex@example.com · Chrome" }instanceId(optional but recommended): a stable ID for this session or seat, up to 200 characters. Re-sending the same one refreshes the session instead of consuming a slot. Omit it to validate without taking a slot, but the cap still applies: when every slot is taken, a call without aninstanceIdis refused with409 session-limittoo. A longer value is treated as omitted, so keep it short.label(optional): up to 120 characters, shown in dashboards. A longer value is ignored.
200 OK
{ "ok": true, "status": "active", "mode": "online", "features": ["pro"], "expiresAt": 1781410876, "sessions": { "used": 2, "max": 3 }, "usage": { "used": 423, "limit": 1000, "period": "2026-05" }}usage.limit is null when the product has no usage cap. period is "YYYY-MM", "YYYY-MM-DD", or "total", depending on
the product configuration.
| Status | error |
Meaning |
|---|---|---|
| 409 | session-limit |
Too many concurrent sessions. Body: sessions: SessionInfo[], max. |
| 404 | invalid-key |
No such key. |
| 403 | revoked / expired |
The license was revoked or has expired. |
| 400 | bad-request |
Missing key. |
POST /v1/usage
Section titled “POST /v1/usage”Record metered usage for the current period and enforce the limit.
Request { "key": "…", "amount": 1 }. amount defaults to 1, is rounded down to an integer, and is capped at 10,000 per call.
200 OK { "allowed": true, "usage": { "used": 424, "limit": 1000, "period": "2026-05" } }
429 { "allowed": false, "usage": { "used": 1000, "limit": 1000, "period": "2026-05" } }: the limit would be exceeded, and
the counter is not incremented.
Errors: 404 invalid-key, 403 revoked / expired.
POST /v1/session/end
Section titled “POST /v1/session/end”End a session (call it on logout) to free a concurrency slot immediately. Sessions also expire automatically after the product’s session TTL (default 15 minutes).
Request { "key": "…", "instanceId": "sess_123" }
200 OK { "ended": true }
Errors: 404 invalid-key, 400 bad-request.
Shared types
Section titled “Shared types”type DeviceInfo = { id: string; name: string; createdAt: number; lastSeenAt: number; current?: boolean; worksUntil?: number };type SessionInfo = { id: string; instanceId: string; label?: string; createdAt: number; lastSeenAt: number };// all timestamps are unix secondsThe signed token payload is specified on Token format.