Signed verdicts
A verdict is Sealcord’s signed answer to “does this licence hold on this machine?”. Your app
checks the signature with the public key it already embeds, so it can trust the answer offline
until valid_until. Use this page to write the check; the TypeScript code for the key check is in
Verify a key offline, and the routes that return verdicts are on
Activate and verify.
The token
Section titled “The token”The activate and status routes return a token:
token = base64url(payload) "." base64url(signature) no paddingsignature = Ed25519( verdict_domain || 0x00 || payload ) 64 bytespayloadis UTF-8 JSON. Verify the bytes you received, then parse them. Never parse and serialise again before checking the signature.verdict_domainis your product’s verdict domain,<product id>/verdict/v1unless you chose another when you added the product (for Acme Notes,acme-notes/verdict/v1).- The public key is the one that checks licence keys. The domain keeps a verdict from ever passing as a licence key, and the other way round.
The payload
Section titled “The payload”The fields come in this order, with nulls written out and times as integer Unix seconds:
| Field | Type | Meaning |
|---|---|---|
v |
1 |
Payload version. Reject any other |
product |
string | Product id, such as acme-notes. Must match the key’s |
licence_id |
UUID string | Must match the key’s licence id |
machine_id |
string | Must match this install’s id |
activation_id |
UUID string or null |
Set when the machine holds a seat |
verdict |
active or revoked |
The answer |
reason |
null, revoked, expired or not_activated |
Why the verdict is not active |
issued_at |
integer | When Sealcord made it |
valid_until |
integer | Until when your app may rely on it. For active, never later than the licence’s expiry |
Checks, in order
Section titled “Checks, in order”- The signature, under your product’s verdict domain, with the embedded public key.
vis1.product,licence_idandmachine_idare this install’s.issued_atis not in the future, allowing 5 minutes of clock skew.- The current time is before
valid_until.
A verdict that fails any check counts as a failed call: ignore it, keep the offline result and
ask again later. Then act on verdict: active runs the app, revoked stops it (any reason).
Trials
Section titled “Trials”A trial licence is an ordinary licence with an end. Its verdict has the same fields and the same
key, and only valid_until differs: an active verdict on a trial is never valid more than a
day past the trial’s end, which covers a payment provider’s word arriving late. When the trial is
paid for, the key does not change. A verdict that has already passed its valid_until when it
arrives fails the last check, so your app treats it as a failed call and asks again.
A worked example
Section titled “A worked example”Use this to test your check. It is signed with a published test key, a seed of 32 bytes of 0x07
that anyone may use, with the verdict domain acme-notes/verdict/v1. The public key is:
ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22cThese are the exact payload bytes, on one line with no spaces:
{"v":1,"product":"acme-notes","licence_id":"3c81e0a2-5b7d-4f19-9a6e-2d8c4b1f7a30","machine_id":"3b0e6f1c-2d4a-4c8e-9f10-7a5b6c7d8e9f","activation_id":"d81d95e0-1c2b-4f3a-8e5d-6c7b8a9f0e1d","verdict":"active","reason":null,"issued_at":1790726400,"valid_until":1791331200}And the token, payload and signature joined by a dot:
eyJ2IjoxLCJwcm9kdWN0IjoiYWNtZS1ub3RlcyIsImxpY2VuY2VfaWQiOiIzYzgxZTBhMi01YjdkLTRmMTktOWE2ZS0yZDhjNGIxZjdhMzAiLCJtYWNoaW5lX2lkIjoiM2IwZTZmMWMtMmQ0YS00YzhlLTlmMTAtN2E1YjZjN2Q4ZTlmIiwiYWN0aXZhdGlvbl9pZCI6ImQ4MWQ5NWUwLTFjMmItNGYzYS04ZTVkLTZjN2I4YTlmMGUxZCIsInZlcmRpY3QiOiJhY3RpdmUiLCJyZWFzb24iOm51bGwsImlzc3VlZF9hdCI6MTc5MDcyNjQwMCwidmFsaWRfdW50aWwiOjE3OTEzMzEyMDB9.Ru9nUFlFOmG96FY0s16Py4g5FzjHhlHwlYBTHrmvc5Wmmt4jeExKW3-88GCeMjVoSUFiz_MTZQHYAAp7kLa9CAYour check should accept this token with that key and domain when the current time is before
1791331200 (7 Oct 2026, 00:00 UTC), so test with a fixed clock such as 1790726400. It should
refuse the token once the time passes 1791331200, or when any byte of the payload changes.
Questions
Section titled “Questions”Why does the app verify the bytes it received?
Section titled “Why does the app verify the bytes it received?”The signature covers those exact bytes. Serialising the parsed JSON again can reorder or reformat it and break the signature, and checking first means no unsigned value is ever used.
Can a verdict for one machine be used on another?
Section titled “Can a verdict for one machine be used on another?”No. The machine_id is signed, and your app refuses a verdict for any id but its own.
Does a new field break my check?
Section titled “Does a new field break my check?”Fields can be added in a later payload version, which has a new v. Your check refuses any v
but 1, and a payload under v 1 keeps the fields above.