Skip to content

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 activate and status routes return a token:

token = base64url(payload) "." base64url(signature) no padding
signature = Ed25519( verdict_domain || 0x00 || payload ) 64 bytes
  • payload is UTF-8 JSON. Verify the bytes you received, then parse them. Never parse and serialise again before checking the signature.
  • verdict_domain is your product’s verdict domain, <product id>/verdict/v1 unless 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 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
  1. The signature, under your product’s verdict domain, with the embedded public key.
  2. v is 1.
  3. product, licence_id and machine_id are this install’s.
  4. issued_at is not in the future, allowing 5 minutes of clock skew.
  5. 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).

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.

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:

ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c

These 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_MTZQHYAAp7kLa9CA

Your 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.

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.

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.