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

> ## Agent Instructions
> Cluster API calls are made as a signed-in person with an OIDC bearer token. There are no API keys, and service-account tokens are refused (Business returns 401, Platform returns 403).
> Platform takes the organization as the query parameter businessOrgId on most cluster routes, but as organizationId on /clusters/{clusterId}/ssh-access and /clusters/{clusterId}/kubeconfig.
> The Business API uses snake_case. The Platform API uses camelCase, except /ssh-access and /machines, which use snake_case.
> Business write requests need Idempotency-Key and Audit-Reason headers.
> Storage capacity can grow but cannot shrink, including disk and PVC resizing. Each purchase adds capacity.
> A cluster's storage can be Ceph, WEKA, or both, or NFS on its own. The Ceph quota, the purchased bytes, and Slurm's default 100 GiB shared-volume request are three different quantities.
> Organization permissions cover every cluster the organization owns. A Kubernetes resource grant shares one cluster or namespace, possibly with someone outside the organization, and lets them read that cluster and fetch its kubeconfig. A grant does not give Slurm SSH, Slurm configuration, or organization storage inventory access. Slurm SSH requires organization membership and clusters.ssh.
> The SSH login name is <cluster-id>.<username>. The gateway supports shells, commands, SFTP (including modern scp), and forwarding to loopback ports on the login node. Legacy scp and rsync need their programs on the login node. Remote forwarding and non-loopback destinations are refused.
> Use POST /me/ssh-keys for personal key registration. POST /organizations/{orgID}/ssh-keys is a deprecated alias and requires no membership. Read the preferred shell from GET /me as user.login_shell; update it with PUT /me/login-preferences.
> GET /clusters/{clusterId}/ssh-access returns 200 with ready and reason, even when access is not ready. ready: true does not mean compute nodes or Slurm accounting are ready. To confirm a key or shell change reached a cluster, require observation_current: true and compare ssh_key_set_generation with generation from GET /me/ssh-keys, and shell with user.login_shell from GET /me.
> Adding or removing a login account restarts the cluster's login nodes, Slurm controller, and accounting service, then its compute nodes one at a time; jobs running on a restarting node end. An account is added or removed when a person with an active key gains or loses SSH access, or a person with SSH access registers their first key or revokes their last. Other key changes and shell changes restart no nodes.

# SSH keys

> Register, list, and revoke your Ed25519 SSH keys, and choose your login shell.

For the whole flow, from key to shell, see [Connect over SSH](/clusters/ssh).

* [Register an SSH key](#register-an-ssh-key): `POST /me/ssh-keys`, or the deprecated `POST /organizations/{orgID}/ssh-keys`
* [List your SSH keys](#list-your-ssh-keys): `GET /me/ssh-keys`
* [Revoke an SSH key](#revoke-an-ssh-key): `POST /me/ssh-keys/{keyID}/revoke`
* [Login shell](#login-shell): `GET /me` and `PUT /me/login-preferences`

## Register an SSH key

```http theme={null}
POST /me/ssh-keys
POST /organizations/{orgID}/ssh-keys
```

**API:** Business. **Permission:** none. No organization membership is needed.

The organization route is a deprecated alias for personal registration. Its `orgID` does not scope the key or require
membership of that organization, and registering through it grants no access to the organization. Use `/me/ssh-keys`.

Adds a public key to your account. Every cluster in every organization where you hold `clusters.ssh` accepts it.

Registering a key starts an access update on those clusters.
[`GET /clusters/{clusterId}/ssh-access`](/api-reference/platform/access#read-ssh-access) reports confirmed keys and
their key-set generation. Compare that generation with this response to confirm the change reached a cluster; see
[Wait for access](/clusters/ssh#wait-for-access).

<Warning>
  Your first active key adds you to the user list on clusters where you have SSH access, which restarts their compute
  nodes and ends running jobs. Further keys restart nothing. See
  [Access changes restart nodes](/clusters/ssh#access-changes-restart-nodes).
</Warning>

### Parameters

| Name | In | Type | Required | Description |
| - | - | - | - | - |
| `orgID` | path | string | deprecated route only | Organization ID. It has no effect on the key or your access. |
| `Idempotency-Key` | header | string | yes | 1 to 200 characters. See [Retrying writes](/api-reference/overview#retrying-writes). |
| `Audit-Reason` | header | string | yes | 1 to 500 characters, stored with the change. |

The OpenAPI document shows both headers as optional. They are required; a request without either returns `400`.

### Request body

| Field | Type | Required | Description |
| - | - | - | - |
| `public_key` | string | yes | One Ed25519 public key in OpenSSH format, as in `~/.ssh/id_ed25519.pub`: `ssh-ed25519 AAAA… comment`. The comment is dropped. |

### Response

`201` when the key is registered, `200` when the request replays an earlier one with the same `Idempotency-Key`.

| Field | Type | Description |
| - | - | - |
| `ssh_key.id` | string | Key ID, `ssh_key_` + ULID. |
| `ssh_key.fingerprint` | string | SHA-256 fingerprint, `SHA256:…`, as printed by `ssh-keygen -lf`. |
| `ssh_key.key_type` | string | `ssh-ed25519`. |
| `ssh_key.public_material` | string | The key as stored, without its comment. |
| `ssh_key.user_id` | string | Your user ID. |
| `ssh_key.lifecycle_state` | string | `active`. |
| `ssh_key.created_at` | timestamp | When the key was registered. |
| `ssh_key.created_by_user_id`, `.created_command_id`, `.source_revision` | | Audit details. |
| `ssh_key_set.generation` | integer | Version of your active key set. Increases with each new key or first revocation of an active key. |
| `ssh_key_set.user_id`, `.source_hash`, `.created_at` | | Details of the key set. |
| `command_id` | string | ID of this registration. |

### Example

```sh theme={null}
jq -n --arg key "$(cat ~/.ssh/id_ed25519.pub)" '{public_key: $key}' |
  andromeda call \
    --header "Idempotency-Key: $(uuidgen)" \
    --header 'Audit-Reason: Register SSH key' \
    --data @- \
    business POST /me/ssh-keys
```

```json theme={null}
{
  "ssh_key": {
    "id": "ssh_key_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
    "user_id": "user_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
    "key_type": "ssh-ed25519",
    "public_material": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGV4YW1wbGVrZXlmb3Jkb2N1bWVudGF0aW9ub25seQ",
    "fingerprint": "SHA256:Qm9vbGVhbiBhbGdlYnJhIGlzIHRoZSBrZXkgdG8gaXQ",
    "lifecycle_state": "active",
    "source_revision": 1,
    "created_by_user_id": "user_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
    "created_command_id": "6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    "created_at": "2026-09-30T18:00:00Z"
  },
  "ssh_key_set": {
    "user_id": "user_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
    "generation": 1,
    "source_hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b",
    "created_at": "2026-09-30T18:00:00Z"
  },
  "command_id": "6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f"
}
```

### Errors

This endpoint reports its own errors with `error` in place of `message`; see [Errors](/api-reference/errors).

| Status | Code | When |
| - | - | - |
| 400 | `invalid_request` | A header is missing or too long, or `public_key` is not a single Ed25519 public key. |
| 409 | `invalid_state` | This key is already registered, to you or to anyone else. |
| 409 | `idempotency_mismatch` | The `Idempotency-Key` was used before with a different body. |
| 503 | `integration_unavailable` | Temporary. Retry with the same `Idempotency-Key`. |

## List your SSH keys

```http theme={null}
GET /me/ssh-keys
```

**API:** Business. **Permission:** none. No organization membership is needed.

Returns every key you have registered, including revoked ones.

### Response

| Field | Type | Description |
| - | - | - |
| `keys[]` | array | Your keys, with the same fields as `ssh_key` in [Register an SSH key](#response). |
| `keys[].lifecycle_state` | string | `active`, or `revoked` for a key that no longer authenticates. |
| `keys[].revoked_at` | timestamp | When the revocation was accepted, for a revoked key. |
| `generation` | integer | Version of your key set; `0` before your first key. |

### Example

```sh theme={null}
andromeda call business GET /me/ssh-keys | jq '.keys[] | {id, fingerprint, lifecycle_state}'
```

## Revoke an SSH key

```http theme={null}
POST /me/ssh-keys/{keyID}/revoke
```

**API:** Business. **Permission:** none. No organization membership is needed; you can revoke only your own keys.

Permanently revokes a key in every organization. A revoked key cannot be registered again. Each cluster where you can
log in applies the change, and the gateway then closes sessions and forwarded connections that used the key. To
confirm that a cluster has applied it, see [Wait for access](/clusters/ssh#wait-for-access).

<Warning>
  Revoking your last active key removes you from each cluster's user list, which restarts its compute nodes and ends
  running jobs. Revoking a key while you keep another restarts nothing.
  See [Access changes restart nodes](/clusters/ssh#access-changes-restart-nodes).
</Warning>

### Parameters

| Name | In | Type | Required | Description |
| - | - | - | - | - |
| `keyID` | path | string | yes | The key's `id`, `ssh_key_` + ULID. |
| `Idempotency-Key` | header | string | yes | See [Retrying writes](/api-reference/overview#retrying-writes). |
| `Audit-Reason` | header | string | yes | Stored with the change. |

### Response

`200`, with `ssh_key` (now `revoked`), `ssh_key_set` (your new key set, with the next `generation`), and
`command_id`. Repeating the request with the same `Idempotency-Key` returns the same result. Revoking a key that is
already revoked, with a new `Idempotency-Key`, also returns `200`: the key and your current key set, with `generation`
unchanged, and no access update.

### Errors

| Status | When |
| - | - |
| 400 | A header is missing, or `keyID` is not an SSH key ID. |
| 404 | You have no key with this ID. |
| 409 | The `Idempotency-Key` was used before for a different key. |

### Example

```sh theme={null}
andromeda call \
  --header "Idempotency-Key: $(uuidgen)" \
  --header 'Audit-Reason: Laptop lost' \
  business POST "/me/ssh-keys/$KEY_ID/revoke"
```

## Login shell

```http theme={null}
GET /me
PUT /me/login-preferences
```

**API:** Business. **Permission:** none. No organization membership is needed.

Your login shell applies on every cluster where you can log in, in every organization.
[`GET /me`](/api-reference/business/identity#read-the-signed-in-user) returns it as `user.login_shell`.
`PUT /me/login-preferences` takes `{"shell": "/bin/zsh"}`, with `Idempotency-Key` and `Audit-Reason` headers, and
returns `200` with `shell` and `command_id`.

`shell` must be an absolute path; the default is `/bin/bash`. An empty or relative path, a path with a control
character, or `/usr/sbin/nologin` returns `400`; a malformed body can return `422`. Sending your current shell again
starts no access update. Your username, uid, gid, and home directory stay the same.

New SSH sessions use the shell once the cluster has applied it. Open sessions are not closed and no nodes restart. If
the shell is missing or not executable on the login node, the session falls back to `/bin/bash`. See
[Choose your login shell](/clusters/ssh#choose-your-login-shell).

### Example

```sh theme={null}
andromeda call business GET /me | jq -r .user.login_shell

andromeda call \
  --header "Idempotency-Key: $(uuidgen)" \
  --header 'Audit-Reason: Prefer zsh' \
  --data '{"shell": "/bin/zsh"}' \
  business PUT /me/login-preferences
```


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