Skip to main content

SDK Upgrade

Attaches a newly purchased license key to an existing end-user account. No session is returned — the app signs in again afterwards via /api/v1/sdk/login, which also re-reads the updated entitlement.

Request

Parameters

Responses

Note the response carries no loggedIn flag, no license block, no tier field, and no session — it returns the write result plus the refreshed subscription list only. The already-linked answer is { alreadyLinked: true, subscriptions } with no upgraded field at all (it’s still a 200, not an error).

Behavior

  • No password hashing tricks here. Unlike login there is no dummy-hash path — unknown user and wrong password give the same INVALID_PASSWORD, but neither increments any lockout counter. Upgrade has no per-identity backoff at all.
  • Key must belong to this product. A valid key for a different product — or a completely unknown one — answers LICENSE_NOT_FOUND (404). This route must not confirm that a stranger’s key exists elsewhere.
  • Already linked = success. If the key is already attached to this user, the response is 200 { alreadyLinked: true, subscriptions }. This prevents a retry after a dropped connection from looking like a failure.
  • Claimed by someone else = 409. LICENSE_IN_USE is a conflict here, and it’s a plain license.update — register is the route with the guarded claim flow.
  • No gates run. Upgrade doesn’t touch license state, blacklists, platform, or version — that’s login’s job afterwards.
  • No session returned. The caller follows up with /api/v1/sdk/login to get a fresh session reflecting the new entitlement. The SDK’s upgrade() returns the raw response data and updates nothing locally.
  • Audit trail. The link writes a LicenseEvent (linked to <user>). No webhook fires on upgrade.
  • Signed responses (v2 Ed25519 scheme) and requireSignedRequests freshness behave exactly as on login — timestamp within 60s, nonce 16–128 chars, unique via Redis SET NX.

Rate Limits

There is no credential limiter and no per-identity backoff on this route. The limiter 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.