SDK Upgrade
/api/v1/sdk/login, which also re-reads the updated entitlement.
Request
Parameters
Responses
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_USEis a conflict here, and it’s a plainlicense.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/loginto get a fresh session reflecting the new entitlement. The SDK’supgrade()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 (
v2Ed25519 scheme) andrequireSignedRequestsfreshness behave exactly as on login — timestamp within 60s, nonce 16–128 chars, unique via RedisSET 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 onerrorCode, never on message text.
