Quickstart
The end-to-end path: company → template → version → license issue → SDK integration. Every command is verified against the API at http://localhost:5100 with the seeded operator admin@certified.local / changeme.
0. Prerequisites
- The CertifiEd API is running with
CERTIFIED_MASTER_KEYset (base64, 32 bytes). curlis installed, plusopensslfor the HMAC-signed heartbeat.
BASE=http://localhost:5100
curl -s $BASE/health# {"status":"ok", ...}1. Operator login
The panel API runs on the HttpOnly cookie certified.auth; store it in the cookie jar cj.txt.
curl -s -c cj.txt -X POST $BASE/api/v1/panel/auth/login \ -H 'Content-Type: application/json' \ -d '{"email":"admin@certified.local","password":"changeme"}'{ "id": "019f69e4-2a5d-7c3c-bb39-93d6829d92a5", "email": "admin@certified.local", "displayName": "Operator", "isOperator": true}From here on, add -b cj.txt to every panel request.
2. The licensee company
A root company can only be created by an operator (parentId: null).
curl -s -b cj.txt -X POST $BASE/api/v1/panel/companies \ -H 'Content-Type: application/json' \ -d '{"name":"Globex Inc","slug":"globex-inc","parentId":null, "contactEmail":"it@globex.example"}'{ "id": "019f6b45-927c-7c2b-978c-91e7689fe7f3", "name": "Globex Inc", "slug": "globex-inc", "parentId": null, "path": "c019f6b45927c7c2b978c91e7689fe7f3", "depth": 0, "status": "Active", "contactEmail": "it@globex.example", "createdAt": "2026-07-16T14:12:29.43+00:00"}Keep the id as COID. A slug is 1–64 characters of [a-z0-9-], must not start or end with a hyphen, and is unique among siblings.
3. The product template
Creating a template automatically generates the first active Ed25519 signing key.
curl -s -b cj.txt -X POST $BASE/api/v1/panel/templates \ -H 'Content-Type: application/json' \ -d '{"name":"Curl Demo","productCode":"curl-demo","description":"demo", "defaultOfflineDays":7,"defaultValidityDays":365}'{ "id": "019f6b45-92bf-758a-8955-131b7c9cc364", "name": "Curl Demo", "productCode": "curl-demo", "defaultOfflineDays": 7, "defaultValidityDays": 365, "status": "Active", "currentVersionId": null, "createdAt": "2026-07-16T14:12:29.50+00:00"}Keep the id as TPLID. currentVersionId is still null — there is no version yet.
4. The template version
A version carries the JSON schema of the config plus its defaults. configSchema and defaults are JSON strings (inner quotes escaped). A newly created version immediately becomes the current one.
curl -s -b cj.txt -X POST $BASE/api/v1/panel/templates/$TPLID/versions \ -H 'Content-Type: application/json' \ -d '{"configSchema":"{\"type\":\"object\"}", "defaults":"{\"features\":[]}","changelog":"v1"}'{ "id": "019f6b45-92f8-71e5-bd7a-aa95cf5644a1", "templateId": "019f6b45-92bf-758a-8955-131b7c9cc364", "version": 1, "configSchema": "{\"type\":\"object\"}", "defaults": "{\"features\":[]}", "signingKeyId": "019f6b45-92c3-7a09-8bdf-24b8ef0b8ba5", "changelog": "v1", "createdAt": "2026-07-16T14:12:29.56+00:00"}5. Issuing a license
The crucial detail: config is a JSON string that gets embedded into the signed token. It is exactly what HasFeature() and GetConfig<T>() read later. Here we ship the export / api features and the limits.maxSeats = 25 limit.
curl -s -b cj.txt -X POST $BASE/api/v1/panel/licenses \ -H 'Content-Type: application/json' \ -d '{ "companyId": "'"$COID"'", "templateId": "'"$TPLID"'", "config": "{\"features\":[\"export\",\"api\"],\"limits\":{\"maxSeats\":25}}", "offlineDays": 7 }'{ "id": "019f6b45-9312-7c41-b1fb-2e2539eaac9d", "licenseKey": "CFED-3NCR-4ZER-BAEA-YREY", "token": "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoi...<three segments>...", "publicKey": "67619cd9aac91085cb5be3b93a9fbca97e5b06c21fcef6a7fdf3b182e16487d8"}We left expiresAt unset → the expiry becomes now + defaultValidityDays of the template. publicKey comes back as hex. Keep licenseKey as KEY and id as LICID.
6. Download the .ced file and the public key
# License file: JSON envelope { licenseKey, token, publicKey }curl -s -b cj.txt $BASE/api/v1/panel/licenses/$LICID/download -o license.ced
# Public key on its own (hex)curl -s -b cj.txt $BASE/api/v1/panel/licenses/$LICID/public-key# {"publicKey":"67619cd9aac9...87d8"}license.ced can be handed to the client application as is — the SDK reads both the JSON envelope and a bare single-line token.
7. The client side: SDK
Normally the SDK does the verification and performs steps 8–9 on its own.
using CertifiEd.Client;
var options = new CertifiEdClientOptions{ ServerUrl = "http://localhost:5100", LicenseFilePath = "license.ced", PublicKey = Convert.FromHexString( "67619cd9aac91085cb5be3b93a9fbca97e5b06c21fcef6a7fdf3b182e16487d8"),};
await using var license = new CertifiEdLicenseClient(options);if (!await license.InitializeAsync()) return; // missing file / bad signature / expired
Console.WriteLine(license.Status); // ActiveConsole.WriteLine(license.HasFeature("export")); // TrueConsole.WriteLine(license.GetConfig<int>("limits.maxSeats")); // 25Details live in the Client SDK section. Below are the same client calls done by hand through the client API.
8. Activation (client API, no HMAC)
activate uses the license key itself as the shared secret — no HMAC signature required.
curl -s -X POST $BASE/api/v1/client/activate \ -H 'Content-Type: application/json' \ -d '{"licenseKey":"'"$KEY"'","hwFingerprint":"demo-fp-001", "machineName":"demo-host"}'{ "activationId": "019f6b45-9362-78f1-aec3-1e3e105a3bf8", "heartbeatToken": "eyJ0eXAiOiJjZXJ0aWZpZWQt...", "heartbeatIntervalSeconds": 3600, "maxOfflineDays": 7}heartbeatToken is the signed offline marker; the SDK caches it next to the license as license.ced.hb. Keep activationId as ACTID.
9. Heartbeat (HMAC-signed)
heartbeat and deactivate are signed with X-CertifiEd-Signature: v1=hex(HMAC_SHA256(key = licenseKey, msg = timestamp)), where the timestamp is in Unix seconds within a ±5 minute window.
TS=$(date +%s)SIG=$(printf '%s' "$TS" | openssl dgst -sha256 -hmac "$KEY" -hex | sed 's/^.*= //')
curl -s -X POST $BASE/api/v1/client/heartbeat \ -H 'Content-Type: application/json' \ -H "X-CertifiEd-Timestamp: $TS" \ -H "X-CertifiEd-Signature: v1=$SIG" \ -d '{"licenseKey":"'"$KEY"'","activationId":"'"$ACTID"'"}'{ "heartbeatToken": "eyJ0eXAiOiJjZXJ0aWZpZWQt..." }HTTP 200 and a fresh marker. Deactivation works the same way through POST /api/v1/client/deactivate and returns 204.
Checking from the panel side
curl -s -b cj.txt $BASE/api/v1/panel/licenses/$LICID/activationscurl -s -b cj.txt "$BASE/api/v1/panel/licenses/$LICID/heartbeats?limit=10"What's next
- Client SDK — integrate it into your own application.
- License protocol — token format, marker, grace period.
- API reference — the complete endpoint catalogue.
- Webhooks — notifications about license events.