## POST /api/v2/vps/{id}/actions/upgrade

**Change VPS plan**

Preview or commit a VPS plan change. Get `{id}` from `GET /api/v2/vps` `data[].id`. First call `GET /api/v2/vps/{id}/actions/upgrade`; send `availablePlans[].slug` as `productSlug`. This route changes the plan only. To change option values such as `bandwidthGb` without changing plan, use `POST /api/v2/vps/{id}/actions/config`. Unsupported fields such as `productId`, `resources`, `draftId`, `send`, or `estimate` are rejected. By default, paid extra bandwidth above the current plan's included bandwidth is kept on top of the target plan. Send `preserveExtraBandwidth: false` only when the customer explicitly chooses to drop paid extra bandwidth; unused paid time is not credited back.

### Related Endpoints

- `GET /api/v2/vps/{id}/actions/upgrade`: Get VPS upgrade options
- `POST /api/v2/vps/{id}/actions/config`: Change VPS resource options
- `GET /api/v2/billing/invoices`: List invoices

### Headers

- `Accept`: application/json
- `Authorization`: Bearer YOUR_API_KEY
- Required API scope: `write:billing`
- `Content-Type`: application/json

### Parameters

- `id` (path, string, required): Public VPS ID from `GET /api/v2/vps` `data[].id`. Do not invent this value; use the exact ID returned by the referenced API response. Example: `vps_01hxa3b4c5d6e7f8g9h0j1k2m3`

### Request Body

- `productSlug` (string, required): Plan slug from `GET /api/v2/vps/{id}/actions/upgrade` `availablePlans[].slug`. Example: `vps-sm`
- `billingCycle` (string, optional): Optional canonical billing cycle for the target plan. Example: `monthly`
  Allowed values: monthly, quarterly, semiannually, annually, biennially, triennially
- `dryRun` (boolean, optional): When true, return pricing and payment availability without committing. Example: `true`
- `cancelExistingInvoice` (boolean, optional): Use only when a 409 `existing_invoice_blocking` response suggests resending with this flag. Example: `true`
- `preserveExtraBandwidth` (boolean, optional): Defaults to true. When the VPS currently has paid bandwidth above the included bandwidth of its current plan, keep that extra bandwidth on top of the target plan. Set false only when the customer explicitly chooses to drop paid extra bandwidth; unused paid time is not credited back. Example: `true`

### Request Examples

#### Preview plan change

```bash
curl -X POST "https://cloud.hostup.se/api/v2/vps/vps_01hxa3b4c5d6e7f8g9h0j1k2m3/actions/upgrade" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "productSlug": "vps-sm",
    "billingCycle": "monthly",
    "dryRun": true
  }'
```

```json
{
  "productSlug": "vps-sm",
  "billingCycle": "monthly",
  "dryRun": true
}
```

#### Commit plan change

```bash
curl -X POST "https://cloud.hostup.se/api/v2/vps/vps_01hxa3b4c5d6e7f8g9h0j1k2m3/actions/upgrade" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "productSlug": "vps-sm",
    "billingCycle": "monthly"
  }'
```

```json
{
  "productSlug": "vps-sm",
  "billingCycle": "monthly"
}
```

#### Preview downgrade and drop paid extra bandwidth

```bash
curl -X POST "https://cloud.hostup.se/api/v2/vps/vps_01hxa3b4c5d6e7f8g9h0j1k2m3/actions/upgrade" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "productSlug": "vps-xs",
    "billingCycle": "monthly",
    "dryRun": true,
    "preserveExtraBandwidth": false
  }'
```

```json
{
  "productSlug": "vps-xs",
  "billingCycle": "monthly",
  "dryRun": true,
  "preserveExtraBandwidth": false
}
```

### Response Schema

- `dryRun` (boolean, optional) Example: `true`
- `currentProduct` (object, optional): Compact current-product reference returned by VPS plan-change preview and commit responses.
- `currentProduct.id` (string,null, required) Example: `vpsprod_01hxa3b4c5d6e7f8g9h0j1k2m3`
- `currentProduct.displayId` (string,null, required): Human-display product identifier when one exists. VPS products usually return null; use `slug` for display and follow-up requests. Example: `null`
- `currentProduct.slug` (string,null, required) Example: `vps-xs`
- `currentProduct.name` (string,null, required) Example: `VPS XS`
- `paymentInvoice` (object,null, optional)
- `renewalInvoice` (object,null, optional)
- `actions` (object, optional)
- `actions.canCommit` (object, optional)
- `actions.canCommit.allowed` (boolean, required) Example: `true`
- `actions.canCommit.reason` (string,null, required) Example: `null`
- `actions.canCommit.code` (string,null, optional): Machine-readable reason code when an action is blocked. Example: `pending_order`
- `warnings` (array<object>, optional): Plan-change adjustments selected by the server, such as preserved storage, preserved paid extra bandwidth, or bandwidth raised to cover current-period usage.
- `warnings[].code` (string, required): Machine-readable adjustment code, for example `package_bandwidth_preserved`, `package_bandwidth_adjusted`, or `package_storage_preserved`. Example: `package_bandwidth_preserved`
- `warnings[].severity` (string, required) Example: `warning`
  Allowed values: warning
- `warnings[].resource` (string, required) Example: `bandwidth`
  Allowed values: bandwidth, storage
- `warnings[].reason` (string, required): Human-readable explanation safe to show to the customer. Example: `This VPS currently has 10 TB extra bandwidth. We will keep that extra bandwidth on the target plan, for 12 TB total.`
- `warnings[].included` (object, optional)
- `warnings[].included.value` (number, required): Amount in the unit named by `unit`. Example: `10240`
- `warnings[].included.unit` (string, required) Example: `GB`
  Allowed values: GB
- `warnings[].current` (object, optional)
- `warnings[].current.value` (number, required): Amount in the unit named by `unit`. Example: `10240`
- `warnings[].current.unit` (string, required) Example: `GB`
  Allowed values: GB
- `warnings[].usage` (object, optional)
- `warnings[].usage.value` (number, required): Amount in the unit named by `unit`. Example: `10240`
- `warnings[].usage.unit` (string, required) Example: `GB`
  Allowed values: GB
- `warnings[].adjusted` (object, required)
- `warnings[].adjusted.value` (number, required): Amount in the unit named by `unit`. Example: `10240`
- `warnings[].adjusted.unit` (string, required) Example: `GB`
  Allowed values: GB
- `warnings[].extra` (object, optional)
- `warnings[].extra.value` (number, required): Amount in the unit named by `unit`. Example: `10240`
- `warnings[].extra.unit` (string, required) Example: `GB`
  Allowed values: GB

### Responses

#### 200 - Plan-change preview or committed plan-change result.
```json
{
  "dryRun": true,
  "currentProduct": {
    "id": "vpsprod_01hxa3b4c5d6e7f8g9h0j1k2m3",
    "displayId": null,
    "slug": "vps-xs",
    "name": "VPS XS"
  },
  "paymentInvoice": {
    "amount": 70,
    "currencyCode": "SEK",
    "paymentMethods": {
      "card": {
        "available": true,
        "reason": null
      },
      "swish": {
        "available": false,
        "reason": "Swish is only available for SEK invoices. This invoice is EUR."
      }
    },
    "availablePaymentMethods": [
      "card",
      "swish"
    ],
    "actions": {
      "canPayWithAvailableMethod": {
        "allowed": true,
        "reason": null
      }
    }
  },
  "renewalInvoice": null,
  "actions": {
    "canCommit": {
      "allowed": true,
      "reason": null
    }
  },
  "warnings": []
}
```

#### 400 - Invalid request. The response body is an RFC 7807 Problem Details document.
```json
{
  "type": "https://developer.hostup.se/errors/invalid_request",
  "title": "Invalid request",
  "status": 400,
  "detail": "The request body failed validation.",
  "code": "invalid_request",
  "instance": "/api/v2/resource",
  "requestId": "req_01hxa3b4c5d6e7f8g9h0j1k2m3",
  "timestamp": "2026-04-27T12:34:56.000Z",
  "errors": [
    {
      "pointer": "/items/0/domainName",
      "detail": "`domainName` is required.",
      "code": "invalid_request"
    }
  ]
}
```

#### 401 - Unauthorized. Authentication is required.
```json
{
  "type": "https://developer.hostup.se/errors/unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Authentication is required.",
  "code": "unauthorized",
  "instance": "/api/v2/resource",
  "requestId": "req_01hxa3b4c5d6e7f8g9h0j1k2m3",
  "timestamp": "2026-04-27T12:34:56.000Z"
}
```

#### 403 - Forbidden. The caller lacks a required scope or does not own the resource.
```json
{
  "type": "https://developer.hostup.se/errors/forbidden",
  "title": "Forbidden",
  "status": 403,
  "detail": "The caller lacks a required scope or does not own the resource.",
  "code": "forbidden",
  "instance": "/api/v2/resource",
  "requestId": "req_01hxa3b4c5d6e7f8g9h0j1k2m3",
  "timestamp": "2026-04-27T12:34:56.000Z"
}
```

#### 404 - Not found. The resource does not exist or is not owned by the caller.
```json
{
  "type": "https://developer.hostup.se/errors/not_found",
  "title": "Not found",
  "status": 404,
  "detail": "The requested resource could not be found.",
  "code": "not_found",
  "instance": "/api/v2/resource",
  "requestId": "req_01hxa3b4c5d6e7f8g9h0j1k2m3",
  "timestamp": "2026-04-27T12:34:56.000Z"
}
```

#### 409 - Conflict. See the Problem Details `code` for the route-specific blocker and recovery fields.
```json
{
  "status": 409,
  "instance": "/api/v2/vps/vps_01hxa3b4c5d6e7f8g9h0j1k2m3/actions/upgrade",
  "requestId": "req_01hxa3b4c5d6e7f8g9h0j1k2m3",
  "timestamp": "2026-04-27T12:34:56.000Z",
  "type": "https://developer.hostup.se/errors/existing_invoice_blocking",
  "title": "Existing invoice blocks plan change",
  "detail": "There is already an unpaid invoice #202600001 for this service. Pay it, or retry with `cancelExistingInvoice: true`.",
  "code": "existing_invoice_blocking",
  "existingInvoice": {
    "id": "inv_01hxa3b4c5d6e7f8g9h0j1k2m3",
    "number": "202600001",
    "amount": 159,
    "currencyCode": "SEK"
  },
  "recovery": {
    "action": "retry_with_cancel_existing_invoice",
    "suggestedBody": {
      "cancelExistingInvoice": true
    }
  }
}
```

#### 429 - Rate limited. Retry after the limit resets. 429 responses include `Retry-After` seconds plus `X-RateLimit-*` headers.
```json
{
  "type": "https://developer.hostup.se/errors/rate_limit_exceeded",
  "title": "Too many requests",
  "status": 429,
  "detail": "Too many requests. Retry after the limit resets.",
  "code": "rate_limit_exceeded",
  "instance": "/api/v2/resource",
  "requestId": "req_01hxa3b4c5d6e7f8g9h0j1k2m3",
  "timestamp": "2026-04-27T12:34:56.000Z"
}
```

#### 500 - Internal error. Retry later or contact support if the issue persists.
```json
{
  "type": "https://developer.hostup.se/errors/internal_error",
  "title": "Internal server error",
  "status": 500,
  "detail": "An unexpected error occurred. Retry later or contact support if the issue persists.",
  "code": "internal_error",
  "instance": "/api/v2/resource",
  "requestId": "req_01hxa3b4c5d6e7f8g9h0j1k2m3",
  "timestamp": "2026-04-27T12:34:56.000Z"
}
```
