> ## Documentation Index
> Fetch the complete documentation index at: https://docs.frostauth.cc/llms.txt
> Use this file to discover all available pages before exploring further.

# SDK Upgrade

> Link a new license key to an existing end-user account.

# SDK Upgrade

```http theme={null}
POST /api/v1/sdk/upgrade
Content-Type: application/json
```

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.

```js theme={null}
await client.upgrade("alex.dev", "••••••••", "FROST-ABCD-1234-WXYZ");
// { upgraded: true, subscriptions: [...] } — then login again
```

## Request

```json theme={null}
{
  "owner": "76fgh4d7",
  "product": "2d0a1800-ffa8-465d-8fcb-908816f.....",
  "username": "alex.dev",
  "password": "••••••••",
  "key": "FROST-ABCD-1234-WXYZ"
}
```

## Parameters

| Field | Required | Description |
| - | - | - |
| `owner` | Yes | Owner account name (or ID). |
| `product` | Yes | Product name, slug, or ID. |
| `username` | Yes | The end-user's username. |
| `password` | Yes | The end-user's password. Required so a borrowed session cannot link keys without proof of ownership. |
| `key` | Yes | The license key to attach. Must belong to this product and be unclaimed. |

## Responses

```text 200 Success theme={null}
{
  "success": true,
  "response": {
    "data": {
      "upgraded": true,
      "subscriptions": [
        { "tier": "", "status": "ACTIVE", "expiresAt": null, "secondsLeft": null }
      ]
    }
  }
}
```

```text 200 Already Linked theme={null}
{
  "success": true,
  "response": {
    "data": {
      "alreadyLinked": true,
      "subscriptions": [
        { "tier": "", "status": "ACTIVE", "expiresAt": null, "secondsLeft": null }
      ]
    }
  }
}
```

```text 400 Bad Request theme={null}
{
  "error": true,
  "response": {
    "code": 400,
    "message": "Missing required fields",
    "errorCode": "MISSING_FIELDS"
  }
}
```

```text 401 Unauthorized theme={null}
{
  "error": true,
  "response": {
    "code": 401,
    "message": "Invalid username or password",
    "errorCode": "INVALID_PASSWORD"
  }
}
```

```text 404 Not Found theme={null}
{
  "error": true,
  "response": {
    "code": 404,
    "message": "License not found",
    "errorCode": "LICENSE_NOT_FOUND"
  }
}
```

```text 409 Conflict theme={null}
{
  "error": true,
  "response": {
    "code": 409,
    "message": "License is already in use",
    "errorCode": "LICENSE_IN_USE"
  }
}
```

```text 429 Rate Limited theme={null}
{
  "error": true,
  "response": {
    "code": 429,
    "message": "Too many requests, slow down",
    "errorCode": "RATE_LIMITED"
  }
}
```

```text 500 Server Error theme={null}
{
  "error": true,
  "response": {
    "code": 500,
    "message": "Upgrade failed"
  }
}
```

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

| Layer | Limit | Keyed by |
| - | - | - |
| Upgrade limiter | 120 per 1 min | Hashed key prefix + IP |
| Abuse guard | Escalating block after repeated failures | IP |

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.

| HTTP | errorCode | Message (default) | Cause |
| - | - | - | - |
| `400` | `MISSING_FIELDS` | `Missing required fields` | `owner`, `product`, `username`, `password`, or `key` missing. |
| `401` | `INVALID_PASSWORD` | `Invalid username or password` | Wrong password or unknown user. |
| `403` | `USER_BANNED` | `This account is banned` | App user status is Banned. |
| `403` | `USER_PAUSED` | `This account is paused` | App user status is Paused. |
| `404` | `LICENSE_NOT_FOUND` | `License not found` | Unknown key, or key belongs to a different product. Note: 404, not 401. |
| `404` | `APP_NOT_FOUND` | `App not registered for this owner` | Owner/product pair doesn't resolve. |
| `409` | `LICENSE_IN_USE` | `License is already in use` | Key already linked to another account. Note: 409, not 403. |
| `429` | `RATE_LIMITED` | `Too many requests, slow down` | Route limiter. |
| `429` | `RATE_LIMITED` | `Too many requests, please try again later` | Abuse guard. Carries `retryAfterSeconds`. |
| `500` | — | `Upgrade failed` | Unhandled exception. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.