SDKs and API

Any language can activate and verify a license

With OwnLicense, activating, renewing and deactivating a license is a small HTTP API on your own Worker. The token it returns is signed with Ed25519, so your app checks it offline with just the public key. SDKs make it shorter, but they're optional.

Status

  • HTTP API

    Exists in the licensing system our own apps use; the generic version for other makers comes with the beta

  • C# SDK (.NET)

    In development

  • Swift SDK

    Planned

  • JS/TS, Python, Kotlin/Java, PHP, Go, Rust/C++

    Later. Until then, the HTTP API works from any language

The API at a glance

JSON over HTTPS on your Worker's domain. No API keys: activation takes the customer's email and license key, and the other calls take the signed token.

Endpoints of the license API
Method and pathWhat it does
POST /v1/activateActivates this device with email and key; returns the signed token
POST /v1/refreshRenews the token (expiry, plan, units); accepts expired tokens
POST /v1/deactivateFrees up this device
POST /v1/freeRequests the free license; the key arrives by email
POST /v1/trialRequests a time-limited trial for this device; the key arrives by email
GET /v1/prices/<app>Paid plans and current prices, to show them in your app
GET /v1/revoked/<app>Signed list of revoked licenses

Activate: request and response

The device id (dev) is a SHA-256 hash of your app id and the machine id, so the raw machine id never leaves the device.

curl
curl -sS https://licenses.example.com/v1/activate \
  -H 'content-type: application/json' \
  -d '{
    "app": "myapp",
    "email": "ana@example.com",
    "key": "MYAPP-7Q2KD-H9XNM-4TRVA-C8PWZ-K",
    "dev": "9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c2e4b6d8f0a1c3e5b7d9f1a3c5e7b9d0f",
    "appVersion": "2.4.0",
    "os": "Windows 11 24H2"
  }'
200
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8

{"token":"eyJ2IjoxLCJhcHAiOiJteWFwcCIsImxpYyI6…","status":"active"}
409
HTTP/1.1 409 Conflict
content-type: application/json; charset=utf-8

{"error":"seats_exhausted","message":"All 3 seats are in use"}

A token you verify offline

The token is base64url(payload) + "." + base64url(signature). The signature covers the payload characters exactly as they are. Your app ships with the public key (and its kid), checks the signature, the app, the device and the expiry, and never waits for the network to start.

app
Your app's slug
lic
License id (the one in the revocation list)
plan / pid
Kind of license (lifetime, subscription, free, trial) and plan code
qty / feat
Units and features the app should unlock
dev
Hash of the device it was activated on
exp
Expiry in Unix seconds, or null for lifetime and free

The token carries no email or other personal data.

payload
{
  "v": 1,
  "kid": "myapp-live-1",
  "app": "myapp",
  "lic": "lic_01h2xcm7q9k3v5b8n4t6r2w0yz",
  "plan": "subscription",
  "pid": "annual",
  "per": "year",
  "qty": 1,
  "feat": ["pro"],
  "dev": "9f2c4e6a…",
  "iat": 1792540800,
  "exp": 1825372800
}

Verifying the token

The same steps in any language. The JavaScript example uses the standard WebCrypto API and works as is; the C# and Swift ones show what the SDKs are meant to look like and may change.

Steps
function isValid(token, now):
    parts = token.split(".")                     # exactly two non-empty parts
    payload = json(base64url_decode(parts[0]))   # read kid; don't trust it yet
    publicKey = EMBEDDED_KEYS[payload.kid]       # unknown kid → invalid
    if not ed25519_verify(publicKey, base64url_decode(parts[1]), ascii(parts[0])): return NO
    if payload.v != 1 or payload.app != THIS_APP:       return NO
    if payload.dev != deviceHash(THIS_APP):         return NO
    if payload.lic in savedRevokedList:                return NO
    if payload.exp != null and payload.exp <= now:     return EXPIRED   # try /v1/refresh
    return YES(pro = payload.plan != "free", units = payload.qty, features = payload.feat)
JavaScript
// WebCrypto (Node 20+, Deno, Bun or a modern browser). Works as is.
const b64url = (s) =>
  Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(s.length / 4) * 4, "=")), (ch) => ch.charCodeAt(0));

async function verify(token, publicKeyB64) {
  const [body, sig] = token.split(".");
  if (!body || !sig) return null;
  const raw = Uint8Array.from(atob(publicKeyB64), (ch) => ch.charCodeAt(0));
  const key = await crypto.subtle.importKey("raw", raw, { name: "Ed25519" }, false, ["verify"]);
  // The signature covers the payload characters exactly as received.
  const ok = await crypto.subtle.verify({ name: "Ed25519" }, key, b64url(sig), new TextEncoder().encode(body));
  return ok ? JSON.parse(new TextDecoder().decode(b64url(body))) : null;
}
C#Illustrative
// Illustrative: what the C# SDK is meant to look like. The API may change.
var licensing = new OwnLicenseClient(
    baseUrl: new Uri("https://licenses.example.com"),
    app: "myapp",
    publicKeys: new() { ["myapp-live-1"] = "BASE64_PUBLIC_KEY" });

// Activate once, with the email and key the customer typed.
var result = await licensing.ActivateAsync(email, key);
if (!result.Ok) ShowError(result.Error); // invalid_key, seats_exhausted…

// On every launch: verify offline, renew in the background.
var license = licensing.Current();        // null = free mode
_ = licensing.RefreshIfNeededAsync();
SwiftIllustrative
// Illustrative: verification with Apple's CryptoKit; the Swift SDK will wrap it.
import CryptoKit
import Foundation

func base64URLDecode(_ s: String) -> Data? {
    var b = s.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
    b += String(repeating: "=", count: (4 - b.count % 4) % 4)
    return Data(base64Encoded: b)
}

func verify(token: String, publicKeyBase64: String) -> [String: Any]? {
    let parts = token.split(separator: ".").map(String.init)
    guard parts.count == 2,
          let keyData = Data(base64Encoded: publicKeyBase64),
          let key = try? Curve25519.Signing.PublicKey(rawRepresentation: keyData),
          let signature = base64URLDecode(parts[1]),
          key.isValidSignature(signature, for: Data(parts[0].utf8)),
          let body = base64URLDecode(parts[0]) else { return nil }
    return try? JSONSerialization.jsonObject(with: body) as? [String: Any]
}

Errors

Errors come back as {"error": "code", "message": "…"}. Your app decides with error (invalid_key, seats_exhausted, expired, rate_limited…) and translates it; message is only for debugging. The full list is in the reference for POST /v1/activate.

Read the documentation

Interested?

Join the waitlist and we'll let you know when the private beta opens. Nobody can use it or sign up for it yet.