Skip to main content

SDK Register

Creates a new end-user account and links it to a license key in one call. The key identifies the product automatically — unlike login, this route needs no owner/product pair. After registration the user is signed in immediately: gate checks, device binding, and session issuance all happen in the same request.
This is not the operator console registration. This creates customer accounts that sign into your application through your software.

Request

The JS SDK fills hwid, os, appVersion, components, and virtual for you:
Optional third argument for the rest: client.register(key, user, pass, { hash, label: "Alex's desktop" }). There is no twofa option here — accounts are created without 2FA.

Parameters

key, hwid, and os are capped at 256 characters.

Responses

Same two extras as login can ride along in data: an offline assertion (cached by the SDK for checkOffline()) and a soft product.updateAvailable hint with latest and optional downloadUrl when autoUpdate is on and your build is behind. session is omitted when the operator’s plan has no token system.

Behavior

  • The license key is looked up by its hash. An unknown key answers LICENSE_NOT_FOUND (401) and writes nothing to the audit log — an unauthenticated caller must not be able to fill another account’s trail with noise. It also adds abuse weight against the caller’s IP.
  • A key already linked to another user answers LICENSE_IN_USE (409, not 403). Two registrations racing on one key cannot both attach: the claim check and the link happen inside the same guarded flow.
  • Username uniqueness is per-product (@@unique([productId, username])), not global. The same username can exist on different products. A taken name and a malformed name give the same answer: 400 USERNAME_TAKEN (Username is taken or invalid) — there is no separate INVALID_USERNAME code.
  • A missing/short password answers 400 WEAK_PASSWORD. A missing key/hwid answers 400 MISSING_FIELDS.
  • After the account is created, the full activation flow runs: gates (kill switch, license state, blacklist, platform, version, build hash, VM block), device binding, and session token issuance. Gate failures get their own accurate code from runGates.
  • There is no per-plan account cap on this route — no USER_LIMIT_REACHED code exists server-side.
  • Session tokens are only returned when the operator’s plan includes the token system. Without it, the response omits the session field.
  • Audit trail: successful registrations write a LicenseEvent (account <user> registered) and emit an activation.succeeded webhook — the same event as a key activation, with user attached. No user.registered event exists.
  • SDK result: register() stores the session/license/device/account/subscriptions and emits registered, then activated. client.snapshot() returns { app, license, product, device, user, account, subscriptions, valid }.

Signed Responses

Every response from this endpoint carries Ed25519 signature headers when a signing key is configured (v2 scheme): The SDK verifies all of this automatically and throws FrostAuthError (carrying the server’s errorCode as code, plus status and requestId) on failure. Unsigned 429s from the rate limiter are treated as retryable, not as forgeries. When requireSignedRequests is enabled for the product, inbound freshness is enforced before the handler runs: timestamp within 60 seconds, nonce 16–128 chars and unique (Redis SET NX, 180s TTL). Stale requests get SESSION_EXPIRED, replays get FORBIDDEN.

Rate Limits

The register limiter’s own answer (Too many registration attempts, slow down) carries no errorCode. The IP floor answers with errorCode: "RATE_LIMITED"; the abuse guard adds retryAfterSeconds.

Errors

Operators can reword any message per product — match on errorCode, never on message text.