The CLV1 protocol is now publicRead the spec

/seller/v1

Seller API

Sell keys, manage buyers and resellers from your own systems, scoped and never twice

Authentication

Create a key in Settings › API keys. It is shown once; only an HMAC of it is stored. Send it in the header on every call, never in the URL (a key in the query string is refused with key_in_url).

curl https://<your API host>/seller/v1/apps \
  -H "Authorization: Bearer clv_live_…"

Keys can be limited to some apps, to a list of IPs or CIDR ranges, and given an expiry. Rotating a key creates a new one and keeps the old one working for 7 days; revoking is immediate.

Scopes

A key does only what its scopes allow; a key with no scope can do nothing. *:read scopes never change anything. Presets: read only, Discord bot (licenses, users, devices), store (licenses), full access.

Writes and idempotency

POST creates or acts, PATCH and PUT change, DELETE removes (licenses are soft-deleted). Send Idempotency-Key with any write: repeating it within 24 hours returns the first answer (marked Idempotent-Replayed: true) instead of acting twice. The same key with a different body is refused.

curl -X POST https://<host>/seller/v1/apps/app_…/licenses \
  -H "Authorization: Bearer clv_live_…" \
  -H "Idempotency-Key: order-1001" \
  -H "Content-Type: application/json" \
  -d '{ "policyId": "pol_…", "count": 1, "email": "buyer@example.com" }'

Full license keys are returned once, in that answer. Store them or send them to the buyer right away.

Rate limits and pages

120 requests per minute per key, 600 on Premium. Every answer carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. Lists take ?limit=50&cursor=… and answer { data, nextCursor }. Every call is logged and visible in the dashboard.

Errors

{ "error": { "code": "forbidden_scope", "message": "…", "details": {}, "requestId": "…" } }
unauthorized401Missing, malformed, expired or revoked key
key_in_url400A key was sent in the query string; use the header
forbidden_scope403The key lacks the scope this endpoint needs
ip_not_allowed403The caller is outside the key's allow list
not_found404Unknown id, or outside the key's apps
validation_failed400The body or query did not match the schema
conflict409Duplicate (username taken, already exists)
idempotency_mismatch422Same Idempotency-Key, different request
idempotency_in_progress409The first request with this key is still running
rate_limited429Over the per-key limit; see RateLimit-* headers
plan_limit402The workspace plan does not allow this
insufficient_credit402A reseller order costs more than the balance

Endpoints

The machine-readable reference is GET /seller/v1/openapi.json (OpenAPI 3.1, generated from the same schemas that validate requests).

MethodPathScope
GET/meany
GET/appsany
GET/apps/{appId}/licenseslicenses:read
POST/apps/{appId}/licenseslicenses:write
GET/licenses/{id}licenses:read
POST/licenses/lookuplicenses:read
POST/licenses/{id}/actionslicenses:write
DELETE/licenses/{id}licenses:write
GET/apps/{appId}/usersusers:read
POST/apps/{appId}/usersusers:write
GET/users/{id}users:read
POST/users/{id}/actionsusers:write
DELETE/users/{id}users:write
GET/apps/{appId}/devicesdevices:read
GET/devices/{id}devices:read
POST/devices/{id}/actionsdevices:write
GET/apps/{appId}/sessionssessions:read
POST/sessions/{id}/revokesessions:write
POST/apps/{appId}/sessions/revoke-allsessions:write
GET/apps/{appId}/variablesvariables:read
PUT/apps/{appId}/variablesvariables:write
DELETE/variables/{id}variables:write
GET/apps/{appId}/rulesrules:read
POST/apps/{appId}/rulesrules:write
DELETE/rules/{id}rules:write
GET/apps/{appId}/webhookswebhooks:read
POST/apps/{appId}/webhookswebhooks:write
DELETE/webhooks/{id}webhooks:write
POST/webhooks/{id}/testwebhooks:write
GET/apps/{appId}/statsstats:read
GET/apps/{appId}/protect/summaryprotect:read
GET/apps/{appId}/protect/eventsprotect:read
GET/resellersresellers:read
POST/resellers/{id}/creditresellers:write
GET/resellers/{id}/ledgerresellers:read

Webhooks

Each delivery is { id, type, createdAt, appId, data } with the header Clavis-Signature: t=<unix>,v1=<hex>, an HMAC-SHA256 of t.body with your signing secret. Check the time is within 5 minutes. Failed deliveries are retried about 15 times over 3 days and can be resent from the dashboard.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

Resellers

A reseller key (created by the reseller in their desk) sees and acts only on the keys that reseller issued, and every key it creates is paid from their credit in the same transaction: an order larger than the balance is refused with insufficient_credit and nothing is charged.

How can we help?

Frequently asked

What is ClavisAUH?

A licensing and authentication service for desktop software: license keys, device binding, end-user sign-in and signed responses. All run from one dashboard

Why can't a cracker fake your server?

The SDK only trusts answers signed with your app's private key. That key never leaves our servers, so a fake server can't sign and gets rejected

What ends up in my binary?

Your app id and public key. Nothing secret, because each device makes its own key pair on first run

Does it work offline?

Yes. Every plan gets a signed offline token for 7 days, and your app checks it locally. Premium lets you choose how long. A revoked license can keep working offline for that time, unless Online-Only Mode is on

Which languages are supported?

JavaScript and TypeScript today. C++, C# and Python are next, on the same open protocol

Is Basic really free?

Yes. No time limit, no card. Move to Premium when you need more

Get in touch