POST /v1/activate

Updated 3 min read

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

  1. Fields as above, otherwise 400 bad_request.
  2. The app exists, otherwise 404 unknown_app.
  3. If the key has the app’s prefix and length, its check digit is verified without touching the database: 404 invalid_key if it doesn’t match.
  4. The license is looked up by the hash of the normalized key: 404 invalid_key if not found.
  5. The email must match the license: 404 invalid_key otherwise (on purpose, it’s not distinguished from a wrong key).
  6. The license must belong to this app: 403 wrong_app.
  7. Add-on keys aren’t activated: 409 addon_key.
  8. State: 403 revoked; 403 expired if canceled or past its expiry; 402 past_due if the payment failed and the grace period is over.
  9. Trial keys only work on the device that requested them: 409 trial_device.
  10. 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"}