Skip to content

Activations and verdicts

An activation is one machine on which a licence is in use. Your app calls three public routes at https://api.sealcord.com, authenticated by the licence key itself: no API key ships in your app. Use this page to wire them up; Signed verdicts says how to check what they answer.

Route What it does Answers
POST /v1/activations Takes a seat on this machine, or refreshes the one it holds 201 seat taken, 200 already held; verdict
POST /v1/activations/status Answers whether the licence still holds on this machine 200 with a verdict
POST /v1/activations/deactivate Frees this machine’s seat 200 { "deactivated": true | false }

Requests and answers are JSON (content-type: application/json), and a body is at most 64 KB. Send an x-request-id header (8 to 64 of A-Z a-z 0-9 . _ -) if you want to quote it to support; the answer echoes it. The same routes are in the API reference.

  1. When the customer enters a key, check it offline. A key that fails is refused without a call.
  2. Activate this machine and keep the answer’s token beside the licence.
  3. On later launches, check the stored token offline with the public key your app embeds. While it is valid, run.
  4. When it has expired, or at most once a day, ask for the status and store the new token.
  5. When the customer signs out or moves to another computer, deactivate.

A network error, a timeout or a 5xx never means the licence is gone. Keep the last good result and try again later: an outage must never lock a customer out.

Generate a random id once per install, store it next to the licence and send it as machine_id on every call. Never send a hardware serial or anything personal; a random UUID is ideal.

Field Rules
licence_key The key as the customer entered it; whitespace is ignored
machine_id 1 to 128 characters of A-Z a-z 0-9 . _ : -
label 1 to 200 characters, a name the customer recognises when freeing seats
os 1 to 64 characters, for example macos 26.1, windows 11 or linux
app_version 1 to 64 characters, your app’s version

label, os and app_version are sent when activating only; status and deactivate take licence_key and machine_id.

Terminal window
curl https://api.sealcord.com/v1/activations \
-H 'content-type: application/json' \
-d '{
"licence_key": "ACN1-…",
"machine_id": "3b0e6f1c-2d4a-4c8e-9f10-7a5b6c7d8e9f",
"label": "Studio iMac",
"os": "macos 26.1",
"app_version": "1.2.0"
}'

The answer is 201 when a seat was taken and 200 when this machine already held one, so calling it again is safe. A repeat also refreshes the label, the system and the version.

{
"activation": {
"id": "d81d95e0-1c2b-4f3a-8e5d-6c7b8a9f0e1d",
"machine_id": "3b0e6f1c-2d4a-4c8e-9f10-7a5b6c7d8e9f",
"activated_at": "2026-09-30T20:48:25.000Z"
},
"verdict": "active",
"reason": null,
"valid_until": "2026-10-07T20:48:25.000Z",
"token": "eyJ2IjoxLC….Ru9nUFlF…"
}

The same call in TypeScript, for any runtime with fetch:

type Verdict = {
verdict: 'active' | 'revoked';
reason: 'revoked' | 'expired' | 'not_activated' | null;
valid_until: string;
token: string;
};
type Machine = { machineId: string; label: string; os: string; appVersion: string };
/** Takes a seat for this machine. Throws on any answer but 200 and 201. */
export async function activate(licenceKey: string, machine: Machine): Promise<Verdict> {
const response = await fetch('https://api.sealcord.com/v1/activations', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
licence_key: licenceKey,
machine_id: machine.machineId,
label: machine.label,
os: machine.os,
app_version: machine.appVersion,
}),
});
if (response.ok === false) {
const { error } = (await response.json()) as { error: { code: string } };
throw new Error(error.code); // match on the code, never the message
}
return (await response.json()) as Verdict;
}

Check the returned token before you trust the verdict field next to it (how).

Terminal window
curl https://api.sealcord.com/v1/activations/status \
-H 'content-type: application/json' \
-d '{ "licence_key": "ACN1-…", "machine_id": "3b0e6f1c-2d4a-4c8e-9f10-7a5b6c7d8e9f" }'

For a key Sealcord issued and knows, the answer is always 200 with a signed verdict: active, or revoked with a reason of revoked, expired or not_activated (this machine holds no seat). On not_activated, call activate once.

{
"verdict": "revoked",
"reason": "not_activated",
"valid_until": "2026-10-07T20:48:25.000Z",
"token": "eyJ2IjoxLC….…"
}

Ask at most once a day per install, and when the customer enters a key. A verdict lasts seven days, never past the licence’s expiry; on a trial, never more than a day past the trial’s end.

Terminal window
curl https://api.sealcord.com/v1/activations/deactivate \
-H 'content-type: application/json' \
-d '{ "licence_key": "ACN1-…", "machine_id": "3b0e6f1c-2d4a-4c8e-9f10-7a5b6c7d8e9f" }'

The answer is 200 { "deactivated": true }, or false when this machine held no seat. It is safe to repeat.

Every error has one shape. Match on code, never on message.

{
"error": {
"code": "seat_limit_reached",
"message": "Every seat of this licence is in use.",
"details": { "limit": 3, "active": 3 },
"request_id": "8f0d6c1e-2a55-4b8e-9d61-0c1f3f6a7b90"
}
}
Status code What your app does
400 validation_failed A bug in the app; details.issues lists the fields
403 licence_revoked, licence_expired The licence no longer holds. Treat it as revoked
404 licence_not_found A genuine key the service does not know. Not a revocation: keep the offline result
409 seat_limit_reached Every seat is taken. Show details.limit and offer to free a seat
422 licence_key_invalid Not a key Sealcord issued, an edited one, or one signed with a signing key you retired
422 licence_wrong_product A key for another product
429 rate_limited Back off for the seconds in the Retry-After-public header. No verdict
5xx internal_error, product_unavailable, service_unavailable No verdict: keep the offline result and retry later with backoff

Your offline check refuses an edited key before any call, so licence_key_invalid mostly means the key was signed with a key you retired: offer to get the customer’s key again, from its licence. Every code is in Errors.

Each of the three routes allows 30 requests a minute per client IP address. Answers carry X-RateLimit-Limit-public, X-RateLimit-Remaining-public and X-RateLimit-Reset-public; a 429 carries Retry-After-public. One status check a day per install stays far below it.

No. The licence key authenticates the call, and the answer is signed with your product’s key. Keep admin API keys on your server.

What if the customer is offline for weeks?

Section titled “What if the customer is offline for weeks?”

The app keeps working on the last verdict until its valid_until, at most seven days after it was made. After that, the app asks again; if the network is down it keeps the offline result of the key check and tries later. What your app does with an expired verdict while offline is your decision, and the safe default is to keep running.

At the app’s next status call. Sealcord answers revoked at once, and the verdict the app holds stops being trusted at its valid_until, at most seven days after it was made.

Does a repeated activation use a second seat?

Section titled “Does a repeated activation use a second seat?”

No. The same machine id on the same licence answers 200 and refreshes its details.