## PATCH /api/v2/vps/{id}/ssh-access

**Update live VPS SSH access**

Atomically remove explicitly selected live SSH access entries and add selected saved account keys. Read GET first and pass its exact revision and opaque IDs. Retained lines keep all OpenSSH options and restrictions. The operation compares the guest file byte-for-byte immediately before installing the result and returns a conflict instead of overwriting a concurrent change. It refuses to stage a partial selection when the live file cannot be verified.

### Related Endpoints

- `GET /api/v2/vps/{id}/ssh-access`: Get live VPS SSH access
- `GET /api/v2/vps/{id}`: Get VPS details
- `GET /api/v2/vps/{id}/iso`: List VPS ISO media

### Headers

- `Accept`: application/json
- `Authorization`: Bearer YOUR_API_KEY
- Required API scope: `write:vm`
- `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

- `revision` (string, required): Exact revision from the latest GET.
- `removeEntryIds` (array<string>, required)
- `addSshKeyIds` (array<string>, required)
- `confirmRemoveAll` (boolean, optional): Required as true when the resulting live inventory has zero public-key entries.

### Request Example

```bash
curl -X PATCH "https://cloud.hostup.se/api/v2/vps/vps_01hxa3b4c5d6e7f8g9h0j1k2m3/ssh-access" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "revision": "string",
    "removeEntryIds": [
      "string"
    ],
    "addSshKeyIds": [
      "string"
    ],
    "confirmRemoveAll": true
  }'
```

```json
{
  "revision": "string",
  "removeEntryIds": [
    "string"
  ],
  "addSshKeyIds": [
    "string"
  ],
  "confirmRemoveAll": true
}
```

### Response Schema

- `available` (boolean, required)
  Allowed values: true
- `loginUser` (string, required) Example: `root`
- `revision` (string, required)
- `entries` (array<object>, required)
- `savedKeys` (array<object>, required)
- `lastOperation` (object, required)
- `lastOperation.added` (integer, required)
- `lastOperation.removed` (integer, required)
- `lastOperation.verified` (boolean, required)
  Allowed values: true
- `lastOperation.metadataSynced` (boolean, required)

### Responses

#### 200 - Updated and verified live SSH access state. The response uses the same base shape as GET and includes `lastOperation`.
```json
{
  "available": true,
  "loginUser": "root",
  "revision": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
  "entries": [],
  "savedKeys": [],
  "lastOperation": {
    "added": 1,
    "removed": 1,
    "verified": true,
    "metadataSynced": true
  }
}
```

#### 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 - The live inventory is unavailable, changed after GET, changed during the update, or could not be verified.
```json
{
  "type": "https://developer.hostup.se/errors/ssh_access_changed",
  "title": "SSH access changed",
  "status": 409,
  "detail": "The server's SSH access list changed after it was loaded. Review the current entries and try again.",
  "code": "ssh_access_changed"
}
```

#### 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"
}
```
