POST /v1/activate
Preliminary documentation: it describes the planned product and will change before the beta.
Activates this device with email + key and returns a signed token.
POST /v1/activate
content-type: application/json
There are no API keys: the email and the license key are the credentials. The body must be a JSON object. Unknown fields are ignored, and empty strings or null count as missing.
Request
| Field | Type | Required | Validation |
|---|---|---|---|
app |
string | yes | ^[a-z0-9-]{1,32}$ |
email |
string | yes | Up to 254 characters. Compared with the license email after trimming and lowercasing |
key |
string | yes | Up to 64 characters. Normalized: uppercase, no spaces or hyphens, O→0, I/L→1 |
dev |
string | yes | ^[0-9a-f]{64}$: SHA-256 of the device, lowercase hex |
deviceName |
string | no | Up to 100 characters |
appVersion |
string | no | Up to 32 characters |
os |
string | no | Up to 64 characters, such as macOS 26.1 |
arch |
string | no | Up to 16 characters, ^[A-Za-z0-9_-]+$ (arm64, x64) |
locale |
string | no | Up to 16 characters, ^[A-Za-z0-9_-]+$ (en-US) |
channel |
string | no | Up to 16 characters, ^[a-z0-9-]+$ (direct, store) |
The Worker also records, as proof of activation, the date, the IP address, the country and the network the request came from. Mention it in your app’s privacy policy.
Checks, in order
- Fields as above, otherwise
400 bad_request. - The app exists, otherwise
404 unknown_app. - If the key has the app’s prefix and length, its check digit is verified without touching the database:
404 invalid_keyif it doesn’t match. - The license is looked up by the hash of the normalized key:
404 invalid_keyif not found. - The email must match the license:
404 invalid_keyotherwise (on purpose, it’s not distinguished from a wrong key). - The license must belong to this app:
403 wrong_app. - Add-on keys aren’t activated:
409 addon_key. - State:
403 revoked;403 expiredif canceled or past its expiry;402 past_dueif the payment failed and the grace period is over. - Trial keys only work on the device that requested them:
409 trial_device. - Devices: activating a device that’s already active doesn’t use another seat. A new device when all seats are taken gives
409 seats_exhausted.
Response 200
{
"token": "eyJ2IjoxLCJhcHAiOiJteWFwcCIs….3vQ2JmXo8w…",
"status": "active"
}
| Field | Meaning |
|---|---|
token |
Signed token. Store it as is |
status |
active, or past_due: the payment failed but the grace period isn’t over (warn without locking) |
Responses carry content-type: application/json; charset=utf-8 and cache-control: no-store.
Errors
Errors come back as JSON with a stable error code and a message in English for debugging only:
{ "error": "invalid_key", "message": "Invalid license key or email" }
error |
HTTP | When |
|---|---|---|
bad_request |
400 | Invalid body or fields |
unknown_app |
404 | The app doesn’t exist |
invalid_key |
404 | Typo, unknown key or a different email |
wrong_app |
403 | The key belongs to another app |
revoked |
403 | The license was revoked |
expired |
403 | Subscription canceled or expired, or trial over |
past_due |
402 | Payment failed and the grace period is over |
seats_exhausted |
409 | No free devices left |
addon_key |
409 | The key is an add-on: activate the main license instead |
trial_device |
409 | The trial key was requested from another device |
rate_limited |
429 | Too many requests from the same IP; comes with Retry-After in seconds |
internal |
500 | Unexpected error; retry with backoff |
Network errors, timeouts or a response that isn’t JSON have no error code: treat them as internal. A 429 without a JSON body comes from Cloudflare’s firewall and means the same as rate_limited.
Example: no seats left
curl -sS -i https://licenses.example.com/v1/activate \
-H 'content-type: application/json' \
-d '{"app":"myapp","email":"ana@example.com","key":"MYAPP-7Q2KD-H9XNM-4TRVA-C8PWZ-K","dev":"0000000000000000000000000000000000000000000000000000000000000004"}'
HTTP/1.1 409 Conflict
content-type: application/json; charset=utf-8
{"error":"seats_exhausted","message":"All 3 seats are in use"}