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.
The flow in your app
Section titled “The flow in your app”- When the customer enters a key, check it offline. A key that fails is refused without a call.
- Activate this machine and keep the answer’s
tokenbeside the licence. - On later launches, check the stored token offline with the public key your app embeds. While it is valid, run.
- When it has expired, or at most once a day, ask for the status and store the new token.
- 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.
Identify the install
Section titled “Identify the install”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.
Activate
Section titled “Activate”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).
Status
Section titled “Status”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.
Deactivate
Section titled “Deactivate”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.
When the API says no
Section titled “When the API says no”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.
Rate limits
Section titled “Rate limits”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.
Questions
Section titled “Questions”Does my app need an API key to activate?
Section titled “Does my app need an API key to activate?”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.
How fast does a revocation reach the app?
Section titled “How fast does a revocation reach the app?”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.