Cluster API

The cluster's servers: members and their maintenance (evacuate, restore, remove, fencing), rolling upgrades, cluster settings and groups, recovery, machines and join tokens, and MAAS provisioning. This page lists 30 operations.

Set variables in the API overview. Download OpenAPI.

Operations

OperationWhat it does
GET /api/v1/cluster/groupsCluster groups and their members (empty on a standalone server).
GET /api/v1/cluster/join-tokensJoin tokens waiting to be used (no secrets).
POST /api/v1/cluster/join-tokensApproves a new cluster member: asks Incus for a single-use join token.
DELETE /api/v1/cluster/join-tokens/{member}Withdraws a new member's approval: revokes its unused join token.
GET /api/v1/cluster/membersMembers with their database role (voter, stand-by, spare), the leader, other roles, failure domain, groups and Incus release; the database majority (how many voters answer, how many are needed, how many more can be lost); and version skew against the pinned Incus release with what blocks an upgrade.
POST /api/v1/cluster/members/{member}/evacuateEvacuates a cluster member through Incus after re-running the dry run: refused while anything blocks it.
GET /api/v1/cluster/members/{member}/evacuationDry run of evacuating a cluster member: per instance, what Incus will do and what it means for the application; what blocks it; what to accept.
POST /api/v1/cluster/members/{member}/fencingConfirms that a failed server is powered off (or cut off from the shared storage).
GET /api/v1/cluster/members/{member}/removalDry run of removing a cluster member for good: what is still on it, what blocks it and what to accept.
POST /api/v1/cluster/members/{member}/removeRemoves an evacuated, empty member from the cluster for good, after the check runs again: refused while anything blocks it.
POST /api/v1/cluster/members/{member}/restoreRestores an evacuated member: Incus moves its instances back and starts them.
GET /api/v1/cluster/recoveryFailed members: whether each is fenced (and the evidence), or why Apelora waits; and per instance whether it restarts on another server (and where) or waits (and why).
GET /api/v1/cluster/settingsCluster-wide settings (offline and healing thresholds, database voters and stand-bys, join-token lifetime, image replication and updates): the value set, Incus's default and whether the default applies.
PATCH /api/v1/cluster/settingsChanges cluster settings through Incus with the admin identity.
POST /api/v1/cluster/upgradeOne step of the rolling upgrade.
GET /api/v1/machinesMachines being enrolled, with their state derived from Incus.
POST /api/v1/machinesRegisters a machine.
DELETE /api/v1/machines/{name}Forgets a machine that is not a cluster member (revoking an unused join token).
GET /api/v1/machines/{name}/admissionThe admission checks for a joined machine (dry run; changes nothing).
POST /api/v1/machines/{name}/admitAdmits a joined machine: re-runs the admission checks and, when none fails, lets Incus place instances on it.
DELETE /api/v1/machines/{name}/approvalWithdraws an approval: revokes the machine's unused join token.
POST /api/v1/machines/{name}/approveApproves a machine: asks Incus for its single-use join token, returned only in this response with the command to run on the server.
GET /api/v1/provisioners/maas/machinesThe machines MAAS manages, with the Apelora machine and Incus member each became.
POST /api/v1/provisioners/maas/machines/{systemId}/commissionStarts commissioning.
POST /api/v1/provisioners/maas/machines/{systemId}/deployInstalls an operating system.
POST /api/v1/provisioners/maas/machines/{systemId}/releaseReturns the machine to MAAS's pool.
POST /api/v1/provisioners/maas/machines/{systemId}/testStarts a hardware test run (burn-in) through MAAS.
GET /api/v1/sectorsThe sectors this control plane knows and whether each answers.
POST /api/v1/sectorsRegisters another Incus cluster as a sector.
DELETE /api/v1/sectors/{name}Forgets a registered sector.

Details

GET /api/v1/cluster/groups

Cluster groups and their members (empty on a standalone server). Read-only; needs cluster.view.

Operation ID: cluster_groups

Needs: cluster.view.

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

curl --fail-with-body -sS -X GET "$APELORA_URL/api/v1/cluster/groups" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json"

TypeScript:

const result = await client.request("cluster_groups", {});

Response 200 — Done; the response holds the result..

Content-Type: application/json.

Example response from a test installation; your IDs, names and times differ.

{
  "source": {
    "provider": "mock",
    "observedAt": "2026-10-28T23:27:11.273398Z"
  },
  "items": [
    {
      "name": "default",
      "description": "mock data",
      "members": [
        "mock-member-01",
        "mock-member-02",
        "mock-member-03"
      ]
    }
  ]
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
401unauthenticated, invalid_token
403forbidden, mfa_required
429rate_limited
502an incus_… code

GET /api/v1/cluster/join-tokens

Join tokens waiting to be used (no secrets). Needs manage-infrastructure.

Operation ID: join_tokens

Needs: manage-infrastructure.

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

curl --fail-with-body -sS -X GET "$APELORA_URL/api/v1/cluster/join-tokens" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json"

TypeScript:

const result = await client.request("join_tokens", {});

Response 200 — Done; the response holds the result..

Content-Type: application/json.

Example response from a test installation; your IDs, names and times differ.

{
  "source": {
    "provider": "mock",
    "observedAt": "2026-10-28T23:26:50.883635Z"
  },
  "items": [
    {
      "memberName": "rack2-01",
      "expiresAt": "2026-10-29T00:26:50.872189Z"
    }
  ]
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
401unauthenticated, invalid_token
403forbidden, mfa_required
429rate_limited
502an incus_… code
503cluster_admin_not_configured, database_not_configured, store_unavailable

POST /api/v1/cluster/join-tokens

Approves a new cluster member: asks Incus for a single-use join token. Needs manage-infrastructure. The token is in this response only.

Operation ID: create_join_token

Needs: manage-infrastructure.

Request body (JSON):

FieldTypeRequiredMeaning
memberNamestringyesName the new member will join with.
{
  "memberName": "rack2-01"
}

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

This request can change state. Check permissions, prerequisites and any required approval before running it.

curl --fail-with-body -sS -X POST "$APELORA_URL/api/v1/cluster/join-tokens" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data-raw '{"memberName":"rack2-01"}'

TypeScript:

const result = await client.request("create_join_token", { body: { memberName: "rack2-01" } });

Response 201 — Created; the response describes what was created..

Content-Type: application/json.

Example response from a test installation; your IDs, names and times differ.

{
  "memberName": "rack2-01",
  "expiresAt": "2026-10-29T00:26:50.872189Z",
  "token": "<token, shown once>",
  "warnings": []
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400invalid_request
401unauthenticated, invalid_token
403forbidden, mfa_required
409approval_invalid, approval_required
429rate_limited
502an incus_… code
503cluster_admin_not_configured, database_not_configured

DELETE /api/v1/cluster/join-tokens/{member}

Withdraws a new member's approval: revokes its unused join token. Needs manage-infrastructure.

Operation ID: revoke_join_token

Needs: manage-infrastructure.

ParameterInTypeRequiredDefaultMeaning
memberpathstringyes—Name the token was issued for

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

This request can change state. Check permissions, prerequisites and any required approval before running it.

curl --fail-with-body -sS -X DELETE "$APELORA_URL/api/v1/cluster/join-tokens/rack2-01" \
  -H "Authorization: Bearer $APELORA_TOKEN"

TypeScript:

await client.request("revoke_join_token", { params: { member: "rack2-01" } });

Response 204 — Done; nothing is returned., with no body.

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400invalid_request
401unauthenticated, invalid_token
403forbidden, mfa_required
404not_found
429rate_limited
502an incus_… code
503cluster_admin_not_configured, database_not_configured

GET /api/v1/cluster/members

Members with their database role (voter, stand-by, spare), the leader, other roles, failure domain, groups and Incus release; the database majority (how many voters answer, how many are needed, how many more can be lost); and version skew against the pinned Incus release with what blocks an upgrade. Read-only; needs cluster.view.

Operation ID: cluster_members

Needs: cluster.view.

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

curl --fail-with-body -sS -X GET "$APELORA_URL/api/v1/cluster/members" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json"

TypeScript:

const result = await client.request("cluster_members", {});

Response 200 — Done; the response holds the result..

Content-Type: application/json.

Example response from a test installation; your IDs, names and times differ. Long lists are cut to their first items.

{
  "source": {
    "provider": "mock",
    "observedAt": "2026-10-28T23:26:50.708546Z"
  },
  "clustered": true,
  "members": [
    {
      "name": "mock-member-01",
      "status": "Online",
      "message": "Fully operational",
      "roles": [
        "database-leader",
        "database"
      ],
      "databaseRole": "voter",
      "leader": true,
      "eventHub": false,
      "ovnChassis": false,
      "databaseExcluded": false,
      "upForDatabase": true,
      "failureDomain": "default",
      "groups": [
        "default"
      ],
      "architecture": "x86_64",
      "held": false,
      "incusVersion": null,
      "versionError": null
    },
    {
      "name": "mock-member-02",
      "status": "Online",
      "message": "Fully operational",
      "roles": [
        "database"
      ],
      "databaseRole": "voter",
      "leader": false,
      "eventHub": false,
      "ovnChassis": false,
      "databaseExcluded": false,
      "upForDatabase": true,
      "failureDomain": "default",
      "groups": [
        "default"
      ],
      "architecture": "x86_64",
      "held": false,
      "incusVersion": null,
      "versionError": null
    }
  ],
  "quorum": {
    "state": "healthy",
    "voters": 3,
    "votersUp": 3,
    "needed": 2,
    "standbys": 0,
    "standbysUp": 0,
    "spares": 0,
    "leader": "mock-member-01",
    "canLose": 1,
    "summary": "3 of 3 voters answer; 2 needed. 1 voter can be lost at once without stopping changes.",
    "nextStep": null,
    "warnings": []
  },
  "versions": {
    "pinned": "7.0.2",
    "groups": [
      {
        "version": null,
        "members": [
          "mock-member-01",
          "mock-member-02",
          "mock-member-03"
        ],
        "againstPin": "unknown"
      }
    ],
    "state": "unknown",
    "summary": "The Incus release of some members could not be read.",
    "ready": false,
    "blockers": [
      "read every member's release first"
    ],
    "notes": []
  },
  "sites": {
    "region": "default",
    "site": "default",
    "sector": "default",
    "members": 3,
    "connectedSites": 1,
    "planned": false,
    "survival": "Connected to site default, sector default (3 members), in region default. Surviving a site means deploying the application again in another site's sector. No other site is connected, so that deployment is not planned."
  },
  "upgrade": {
    "running": false,
    "order": [
      "mock-member-02",
      "mock-member-03",
      "mock-member-01"
    ],
    "next": "start",
    "summary": "No upgrade is in progress. Starting one walks the members below, spares and stand-bys first and the database leader last. Apelora evacuates and restores; the package upgrade happens on the server."
  }
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
401unauthenticated, invalid_token
403forbidden, mfa_required
429rate_limited
500invalid_place, store_error
502an incus_… code
503database_not_configured

POST /api/v1/cluster/members/{member}/evacuate

Evacuates a cluster member through Incus after re-running the dry run: refused while anything blocks it. Needs manage-infrastructure and the admin identity; audited.

Operation ID: evacuate_member

Needs: manage-infrastructure and the admin identity; audited.

ParameterInTypeRequiredDefaultMeaning
memberpathstringyes—Cluster member name

Request body (JSON):

FieldTypeRequiredMeaning
confirmstringyesThe member's name again, typed by the administrator.
forcebooleannoEvacuate although the dry run found blockers (a workload may lose service). Needs the unsafe-evacuation permission and an approval from another person.
{
  "confirm": "mock-member-02",
  "force": false
}

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

This request can change state. Check permissions, prerequisites and any required approval before running it.

curl --fail-with-body -sS -X POST "$APELORA_URL/api/v1/cluster/members/mock-member-02/evacuate" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data-raw '{"confirm":"mock-member-02","force":false}'

TypeScript:

const result = await client.request("evacuate_member", { params: { member: "mock-member-02" }, body: { confirm: "mock-member-02", force: false } });

Response 202 — Accepted: the work is queued; the response says how to follow it..

Content-Type: application/json.

Example response from a test installation; your IDs, names and times differ.

{
  "member": "mock-member-02",
  "operation": "00000000-0000-4000-8000-000000000001",
  "drain": "No application replica moves, so none was taken out of a service address."
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400confirmation_mismatch, invalid_request
401unauthenticated, invalid_token
403forbidden, mfa_required
404not_found
409approval_invalid, approval_required, evacuation_blocked
429rate_limited
502an incus_… code
503cluster_admin_not_configured, database_not_configured

GET /api/v1/cluster/members/{member}/evacuation

Dry run of evacuating a cluster member: per instance, what Incus will do and what it means for the application; what blocks it; what to accept. Changes nothing.

Operation ID: preview_evacuation

Needs: a signed-in caller; the server decides what that caller may see.

ParameterInTypeRequiredDefaultMeaning
memberpathstringyes—Cluster member name

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

curl --fail-with-body -sS -X GET "$APELORA_URL/api/v1/cluster/members/mock-member-03/evacuation" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json"

TypeScript:

const result = await client.request("preview_evacuation", { params: { member: "mock-member-03" } });

Response 200 — Done; the response holds the result..

Content-Type: application/json.

Example response from a test installation; your IDs, names and times differ.

{
  "source": {
    "provider": "mock",
    "observedAt": "2026-10-28T23:26:50.730239Z"
  },
  "plan": {
    "member": "mock-member-03",
    "targets": [
      "mock-member-01",
      "mock-member-02"
    ],
    "items": [
      {
        "project": "default",
        "instance": "mock-db-01",
        "status": "Running",
        "mode": "auto",
        "outcome": "moves if it can; stopped during the move",
        "impact": "not Apelora's"
      }
    ],
    "blocked": [],
    "warnings": []
  }
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400invalid_request
401unauthenticated, invalid_token
403forbidden, mfa_required
404not_found
429rate_limited
502an incus_… code

POST /api/v1/cluster/members/{member}/fencing

Confirms that a failed server is powered off (or cut off from the shared storage). This fences it: Apelora then restarts its modules on shared storage on other servers. Refused unless Incus reports the member Offline. Needs cluster.fence_confirm and another person's approval; the member's name typed again; audited.

Operation ID: confirm_fenced

Needs: cluster.fence_confirm and another person's approval; the member's name typed again; audited.

ParameterInTypeRequiredDefaultMeaning
memberpathstringyes—Cluster member name

Request body (JSON):

FieldTypeRequiredMeaning
confirmstringyesThe member's name again, typed by the person.
poweredOffbooleanyesMust be true: the server is switched off (or cut off from the shared storage).
{
  "confirm": "mock-member-01",
  "poweredOff": true
}

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

This request can change state. Check permissions, prerequisites and any required approval before running it.

curl --fail-with-body -sS -X POST "$APELORA_URL/api/v1/cluster/members/mock-member-01/fencing" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data-raw '{"confirm":"mock-member-01","poweredOff":true}'

TypeScript:

const result = await client.request("confirm_fenced", { params: { member: "mock-member-01" }, body: { confirm: "mock-member-01", poweredOff: true } });

Response 200 — Done; the response holds the result..

Content-Type: application/json.

Example response (placeholder values that show the shape):

{
  "fencing": {
    "condition": "waiting",
    "reason": "example",
    "evidence": {
      "at": 1,
      "kind": "storage-lease",
      "summary": "example",
      "confirmedBy": "example",
      "generation": 0,
      "hostId": 0,
      "lockspace": "example",
      "observedBy": "example",
      "provider": "example"
    },
    "leaseState": "free",
    "nextStep": "example",
    "observedAt": 1,
    "observedBy": "example"
  },
  "items": [
    {
      "action": "restart",
      "instance": "example",
      "project": "apelora",
      "reason": "example",
      "application": "example",
      "module": "example",
      "target": "example"
    }
  ],
  "member": "example",
  "status": "example",
  "confirmedBy": "example",
  "decidedAt": "2026-01-01T09:00:00Z",
  "detectedAt": "2026-01-01T09:00:00Z",
  "evidence": {
    "at": 1,
    "kind": "storage-lease",
    "summary": "example",
    "confirmedBy": "example",
    "generation": 0,
    "hostId": 0,
    "lockspace": "example",
    "observedBy": "example",
    "provider": "example"
  },
  "failure": "example",
  "fencedAt": "2026-01-01T09:00:00Z",
  "lastHeartbeat": "2026-01-01T09:00:00Z"
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400confirmation_mismatch, invalid_request
401unauthenticated, invalid_token
403forbidden, mfa_required
404not_found
409approval_invalid, approval_required, member_not_offline
429rate_limited
502an incus_… code
503database_not_configured

GET /api/v1/cluster/members/{member}/removal

Dry run of removing a cluster member for good: what is still on it, what blocks it and what to accept. Changes nothing.

Operation ID: preview_member_removal

Needs: a signed-in caller; the server decides what that caller may see.

ParameterInTypeRequiredDefaultMeaning
memberpathstringyes—Cluster member name

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

curl --fail-with-body -sS -X GET "$APELORA_URL/api/v1/cluster/members/mock-member-03/removal" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json"

TypeScript:

const result = await client.request("preview_member_removal", { params: { member: "mock-member-03" } });

Response 200 — Done; the response holds the result..

Content-Type: application/json.

Example response from a test installation; your IDs, names and times differ.

{
  "source": {
    "provider": "mock",
    "observedAt": "2026-10-28T23:26:50.783442Z"
  },
  "plan": {
    "member": "mock-member-03",
    "status": "Online",
    "instances": [
      "default/mock-db-01"
    ],
    "volumes": [],
    "blocked": [
      "evacuate it first, so Incus moves its instances away",
      "1 instance is still on it (default/mock-db-01); an evacuation leaves instances whose volumes are on the member stopped in place"
    ],
    "warnings": [
      "the cluster keeps 2 database members (fewer than three): losing one more member can stop it",
      "2 members remain: replicas have fewer places to spread over",
      "to use the server again, reinstall it and enrol it as a new machine"
    ]
  }
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400invalid_request
401unauthenticated, invalid_token
403forbidden, mfa_required
404not_found
429rate_limited
502an incus_… code

POST /api/v1/cluster/members/{member}/remove

Removes an evacuated, empty member from the cluster for good, after the check runs again: refused while anything blocks it. Needs cluster.manage, the admin identity and the member's name typed again; audited.

Operation ID: remove_member

Needs: cluster.manage, the admin identity and the member's name typed again; audited.

ParameterInTypeRequiredDefaultMeaning
memberpathstringyes—Cluster member name

Request body (JSON):

FieldTypeRequiredMeaning
confirmstringyesThe member's name again, typed by the administrator.
forcebooleannoEvacuate although the dry run found blockers (a workload may lose service). Needs the unsafe-evacuation permission and an approval from another person.
{
  "confirm": "mock-member-03",
  "force": false
}

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

This request can change state. Check permissions, prerequisites and any required approval before running it.

curl --fail-with-body -sS -X POST "$APELORA_URL/api/v1/cluster/members/mock-member-03/remove" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Content-Type: application/json" \
  --data-raw '{"confirm":"mock-member-03","force":false}'

TypeScript:

await client.request("remove_member", { params: { member: "mock-member-03" }, body: { confirm: "mock-member-03", force: false } });

Response 204 — Done; nothing is returned., with no body.

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400confirmation_mismatch, invalid_request
401unauthenticated, invalid_token
403forbidden, mfa_required
404not_found
409approval_invalid, approval_required, removal_blocked
429rate_limited
502an incus_… code
503cluster_admin_not_configured, database_not_configured

POST /api/v1/cluster/members/{member}/restore

Restores an evacuated member: Incus moves its instances back and starts them. Needs manage-infrastructure and the admin identity; audited.

Operation ID: restore_member

Needs: manage-infrastructure and the admin identity; audited.

ParameterInTypeRequiredDefaultMeaning
memberpathstringyes—Cluster member name

Request body (JSON):

FieldTypeRequiredMeaning
confirmstringyesThe member's name again, typed by the administrator.
forcebooleannoEvacuate although the dry run found blockers (a workload may lose service). Needs the unsafe-evacuation permission and an approval from another person.
{
  "confirm": "mock-member-02",
  "force": false
}

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

This request can change state. Check permissions, prerequisites and any required approval before running it.

curl --fail-with-body -sS -X POST "$APELORA_URL/api/v1/cluster/members/mock-member-02/restore" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data-raw '{"confirm":"mock-member-02","force":false}'

TypeScript:

const result = await client.request("restore_member", { params: { member: "mock-member-02" }, body: { confirm: "mock-member-02", force: false } });

Response 202 — Accepted: the work is queued; the response says how to follow it..

Content-Type: application/json.

Example response from a test installation; your IDs, names and times differ.

{
  "member": "mock-member-02",
  "operation": "00000000-0000-4000-8000-000000000002",
  "drain": "No application replica moves, so none was taken out of a service address."
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400confirmation_mismatch, invalid_request
401unauthenticated, invalid_token
403forbidden, mfa_required
404—
429rate_limited
502an incus_… code
503cluster_admin_not_configured, database_not_configured

GET /api/v1/cluster/recovery

Failed members: whether each is fenced (and the evidence), or why Apelora waits; and per instance whether it restarts on another server (and where) or waits (and why). Read-only; needs cluster.view.

Operation ID: cluster_recovery

Needs: cluster.view.

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

curl --fail-with-body -sS -X GET "$APELORA_URL/api/v1/cluster/recovery" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json"

TypeScript:

const result = await client.request("cluster_recovery", {});

Response 200 — Done; the response holds the result..

Content-Type: application/json.

Example response from a test installation; your IDs, names and times differ.

{
  "source": {
    "provider": "mock",
    "observedAt": "2026-10-28T23:26:50.936724Z"
  },
  "items": []
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
401unauthenticated, invalid_token
403forbidden, mfa_required
429rate_limited
502an incus_… code

GET /api/v1/cluster/settings

Cluster-wide settings (offline and healing thresholds, database voters and stand-bys, join-token lifetime, image replication and updates): the value set, Incus's default and whether the default applies. Read with the admin identity, since Incus shows its configuration only to an unrestricted one (503 cluster_admin_not_configured without it); only these keys are returned. Read-only; needs cluster.view.

Operation ID: cluster_settings

Needs: cluster.view.

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

curl --fail-with-body -sS -X GET "$APELORA_URL/api/v1/cluster/settings" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json"

TypeScript:

const result = await client.request("cluster_settings", {});

Response 200 — Done; the response holds the result..

Content-Type: application/json.

Example response from a test installation; your IDs, names and times differ. Long lists are cut to their first items.

{
  "source": {
    "provider": "mock",
    "observedAt": "2026-10-28T23:26:50.928806Z"
  },
  "identity": "admin",
  "items": [
    {
      "key": "cluster.offline_threshold",
      "label": "Offline after (seconds)",
      "meaning": "How long a member may miss heartbeats before it counts as offline.",
      "value": null,
      "default": "20",
      "usesDefault": true
    },
    {
      "key": "cluster.healing_threshold",
      "label": "Evacuate offline members after (seconds)",
      "meaning": "0 leaves offline members alone; otherwise Incus evacuates them after this long.",
      "value": null,
      "default": "0",
      "usesDefault": true
    }
  ]
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
401unauthenticated, invalid_token
403forbidden, mfa_required
429rate_limited
502an incus_… code
503cluster_admin_not_configured, database_not_configured, store_unavailable

PATCH /api/v1/cluster/settings

Changes cluster settings through Incus with the admin identity. Every key must be one the Servers page lists and every value is checked. Settings that already hold the value are left out; nothing to change is refused. With dryRun it only reports (what each becomes and what to watch for). Applying needs cluster.sensitive_change and another person's approval of cluster-settings:<keys joined by /> (Apelora-Approval), spent before the change, and is audited with the old and new values.

Operation ID: change_cluster_settings

Needs: cluster.sensitive_change and another person's approval of cluster-settings:<keys joined by /> (Apelora-Approval), spent before the change, and is audited with the old and new values.

Request body (JSON):

FieldTypeRequiredMeaning
changesobjectyes—
changes.<key>string or nullnoAny key.
dryRunbooleannoOnly report what would change and what to watch for; nothing is changed and no approval is needed.
{
  "changes": {
    "cluster.join_token_expiry": "3H"
  },
  "dryRun": true
}

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

This request can change state. Check permissions, prerequisites and any required approval before running it.

curl --fail-with-body -sS -X PATCH "$APELORA_URL/api/v1/cluster/settings" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data-raw '{"changes":{"cluster.join_token_expiry":"3H"},"dryRun":true}'

TypeScript:

const result = await client.request("change_cluster_settings", { body: { changes: { "cluster.join_token_expiry": "3H" }, dryRun: true } });

Response 200 — Done; the response holds the result..

Content-Type: application/json.

Example response from a test installation; your IDs, names and times differ.

{
  "applied": false,
  "changes": [
    {
      "key": "cluster.join_token_expiry",
      "label": "Join tokens expire after",
      "from": "1H",
      "to": "3H",
      "warnings": [
        "Apelora recommends join tokens that last an hour or less."
      ]
    }
  ],
  "approvalTarget": "cluster-settings:cluster.join_token_expiry"
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400invalid_request, invalid_setting
401unauthenticated, invalid_token
403forbidden, mfa_required
409approval_invalid, approval_required, nothing_to_change
429rate_limited
502an incus_… code
503cluster_admin_not_configured, database_not_configured

POST /api/v1/cluster/upgrade

One step of the rolling upgrade. Apelora evacuates and restores. It does not install Incus: upgraded records that the package was upgraded on the server. Needs cluster.evacuate.

Operation ID: upgrade_cluster

Needs: cluster.evacuate.

Request body (JSON):

FieldTypeRequiredMeaning
actionstringyesstart, evacuate, upgraded, restore or abandon.
confirmstringyesupgrade to start or abandon; the member's name for the other actions.
memberstringnoThe member, for evacuate, upgraded and restore.
{
  "action": "start",
  "confirm": "upgrade"
}

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

This request can change state. Check permissions, prerequisites and any required approval before running it.

curl --fail-with-body -sS -X POST "$APELORA_URL/api/v1/cluster/upgrade" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data-raw '{"action":"start","confirm":"upgrade"}'

TypeScript:

const result = await client.request("upgrade_cluster", { body: { action: "start", confirm: "upgrade" } });

Response 200 — Done; the response holds the result..

Content-Type: application/json.

Example response (placeholder values that show the shape):

{
  "next": "example",
  "order": [
    "example"
  ],
  "running": false,
  "summary": "example",
  "member": "example"
}

Response 202 — Accepted: the work is queued; the response says how to follow it..

Content-Type: application/json.

Example response (placeholder values that show the shape):

{
  "next": "example",
  "order": [
    "example"
  ],
  "running": false,
  "summary": "example",
  "member": "example"
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400confirmation_mismatch, invalid_request
401unauthenticated, invalid_token
403forbidden, mfa_required
404not_found
409evacuation_blocked, no_upgrade, not_upgraded, upgrade_blocked, upgrade_in_progress, upgrade_step
429rate_limited
502an incus_… code
503cluster_admin_not_configured, database_not_configured

GET /api/v1/machines

Machines being enrolled, with their state derived from Incus. Needs manage-infrastructure.

Operation ID: machines

Needs: manage-infrastructure.

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

curl --fail-with-body -sS -X GET "$APELORA_URL/api/v1/machines" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json"

TypeScript:

const result = await client.request("machines", {});

Response 200 — Done; the response holds the result..

Content-Type: application/json.

Example response from a test installation; your IDs, names and times differ.

{
  "source": {
    "provider": "mock",
    "observedAt": "2026-10-28T23:27:11.703874Z"
  },
  "items": [
    {
      "name": "mock-member-03",
      "provider": "manual",
      "providerRef": null,
      "capabilities": {
        "discovers": false,
        "installs": false,
        "powers": false
      },
      "description": "First rack, slot 3",
      "state": "eligible",
      "memberStatus": "Online",
      "architecture": "x86_64",
      "nextStep": "None.",
      "warnings": [],
      "registeredBy": "token:docs-verifier",
      "registeredAt": "2026-10-28T23:26:50.972023Z",
      "approvedBy": null,
      "approvedAt": null,
      "approvalExpiresAt": null,
      "admittedBy": "token:docs-verifier",
      "admittedAt": "2026-10-28T23:26:51.011888Z"
    }
  ]
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
401unauthenticated, invalid_token
403forbidden, mfa_required
429rate_limited
502an incus_… code
503cluster_admin_not_configured, database_not_configured

POST /api/v1/machines

Registers a machine. A name that is already an eligible member is adopted as admitted. Needs manage-infrastructure; audited.

Operation ID: register_machine

Needs: manage-infrastructure; audited.

Request body (JSON):

FieldTypeRequiredMeaning
descriptionstringno—
namestringyesThe name it will have as a cluster member.
providerone of manual, maas or nullno—
systemIdstring or nullnoFor a maas machine: its MAAS system id (from GET /api/v1/provisioners/maas/machines).
{
  "name": "rack2-01",
  "description": "Second rack, slot 1",
  "provider": "manual"
}

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

This request can change state. Check permissions, prerequisites and any required approval before running it.

curl --fail-with-body -sS -X POST "$APELORA_URL/api/v1/machines" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data-raw '{"name":"rack2-01","description":"Second rack, slot 1","provider":"manual"}'

TypeScript:

const result = await client.request("register_machine", { body: { name: "rack2-01", description: "Second rack, slot 1", provider: "manual" } });

Response 201 — Created; the response describes what was created..

Content-Type: application/json.

Example response from a test installation; your IDs, names and times differ.

{
  "name": "rack2-01",
  "provider": "manual",
  "providerRef": null,
  "capabilities": {
    "discovers": false,
    "installs": false,
    "powers": false
  },
  "description": "Second rack, slot 1",
  "state": "registered",
  "memberStatus": null,
  "architecture": null,
  "nextStep": "Install Incus on the server, then approve it.",
  "warnings": [],
  "registeredBy": "token:docs-verifier",
  "registeredAt": "2026-10-28T23:26:50.946071Z",
  "approvedBy": null,
  "approvedAt": null,
  "approvalExpiresAt": null,
  "admittedBy": null,
  "admittedAt": null
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400invalid_request, unknown_machine
401unauthenticated, invalid_token
403forbidden, mfa_required
409—
429rate_limited
502provisioner_unavailable, an incus_… code
503cluster_admin_not_configured, provisioner_not_configured, database_not_configured

DELETE /api/v1/machines/{name}

Forgets a machine that is not a cluster member (revoking an unused join token). Needs manage-infrastructure; audited.

Operation ID: remove_machine

Needs: manage-infrastructure; audited.

ParameterInTypeRequiredDefaultMeaning
namepathstringyes—Machine name

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

This request can change state. Check permissions, prerequisites and any required approval before running it.

curl --fail-with-body -sS -X DELETE "$APELORA_URL/api/v1/machines/rack2-01" \
  -H "Authorization: Bearer $APELORA_TOKEN"

TypeScript:

await client.request("remove_machine", { params: { name: "rack2-01" } });

Response 204 — Done; nothing is returned., with no body.

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400invalid_request
401unauthenticated, invalid_token
403forbidden, mfa_required
404—
409approval_invalid, approval_required, still_a_member
429rate_limited
502an incus_… code
503cluster_admin_not_configured, database_not_configured

GET /api/v1/machines/{name}/admission

The admission checks for a joined machine (dry run; changes nothing). Needs manage-infrastructure.

Operation ID: preview_machine_admission

Needs: manage-infrastructure.

ParameterInTypeRequiredDefaultMeaning
namepathstringyes—Machine name

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

curl --fail-with-body -sS -X GET "$APELORA_URL/api/v1/machines/mock-member-03/admission" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json"

TypeScript:

const result = await client.request("preview_machine_admission", { params: { name: "mock-member-03" } });

Response 200 — Done; the response holds the result..

Content-Type: application/json.

Example response from a test installation; your IDs, names and times differ. Long lists are cut to their first items.

{
  "source": {
    "provider": "mock",
    "observedAt": "2026-10-28T23:26:50.997249Z"
  },
  "name": "mock-member-03",
  "admissible": true,
  "checks": [
    {
      "id": "member-online",
      "title": "Member online",
      "status": "pass",
      "detail": "online"
    },
    {
      "id": "incus-version",
      "title": "Incus version",
      "status": "not-checked",
      "detail": "the member did not report its version"
    }
  ]
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400invalid_request
401unauthenticated, invalid_token
403forbidden, mfa_required
404—
409not_a_member
429rate_limited
502an incus_… code
503cluster_admin_not_configured, database_not_configured

POST /api/v1/machines/{name}/admit

Admits a joined machine: re-runs the admission checks and, when none fails, lets Incus place instances on it. Needs manage-infrastructure; audited.

Operation ID: admit_machine

Needs: manage-infrastructure; audited.

ParameterInTypeRequiredDefaultMeaning
namepathstringyes—Machine name

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

This request can change state. Check permissions, prerequisites and any required approval before running it.

curl --fail-with-body -sS -X POST "$APELORA_URL/api/v1/machines/mock-member-03/admit" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json"

TypeScript:

const result = await client.request("admit_machine", { params: { name: "mock-member-03" } });

Response 200 — Done; the response holds the result..

Content-Type: application/json.

Example response from a test installation; your IDs, names and times differ. Long lists are cut to their first items.

{
  "source": {
    "provider": "mock",
    "observedAt": "2026-10-28T23:26:51.009919Z"
  },
  "name": "mock-member-03",
  "admissible": true,
  "checks": [
    {
      "id": "member-online",
      "title": "Member online",
      "status": "pass",
      "detail": "online"
    },
    {
      "id": "incus-version",
      "title": "Incus version",
      "status": "not-checked",
      "detail": "the member did not report its version"
    }
  ]
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400invalid_request
401unauthenticated, invalid_token
403forbidden, mfa_required
404—
409admission_blocked, not_a_member
429rate_limited
502an incus_… code
503cluster_admin_not_configured, database_not_configured

DELETE /api/v1/machines/{name}/approval

Withdraws an approval: revokes the machine's unused join token. Needs manage-infrastructure; audited.

Operation ID: withdraw_machine_approval

Needs: manage-infrastructure; audited.

ParameterInTypeRequiredDefaultMeaning
namepathstringyes—Machine name

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

This request can change state. Check permissions, prerequisites and any required approval before running it.

curl --fail-with-body -sS -X DELETE "$APELORA_URL/api/v1/machines/rack2-01/approval" \
  -H "Authorization: Bearer $APELORA_TOKEN"

TypeScript:

await client.request("withdraw_machine_approval", { params: { name: "rack2-01" } });

Response 204 — Done; nothing is returned., with no body.

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400invalid_request
401unauthenticated, invalid_token
403forbidden, mfa_required
404—
429rate_limited
502an incus_… code
503cluster_admin_not_configured, database_not_configured

POST /api/v1/machines/{name}/approve

Approves a machine: asks Incus for its single-use join token, returned only in this response with the command to run on the server. Needs manage-infrastructure; audited without the token.

Operation ID: approve_machine

Needs: manage-infrastructure; audited without the token.

ParameterInTypeRequiredDefaultMeaning
namepathstringyes—Machine name

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

This request can change state. Check permissions, prerequisites and any required approval before running it.

curl --fail-with-body -sS -X POST "$APELORA_URL/api/v1/machines/rack2-01/approve" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json"

TypeScript:

const result = await client.request("approve_machine", { params: { name: "rack2-01" } });

Response 201 — Created; the response describes what was created..

Content-Type: application/json.

Example response from a test installation; your IDs, names and times differ.

{
  "name": "rack2-01",
  "expiresAt": "2026-10-29T00:26:51.035352Z",
  "token": "<token, shown once>",
  "enrolCommand": "sudo bash scripts/enrol/enrol-member.sh --name rack2-01 --address <this server's cluster address> --pool-size <size of the local loop pool, such as 20GiB>",
  "warnings": []
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400invalid_request
401unauthenticated, invalid_token
403forbidden, mfa_required
404—
409already_a_member, approval_invalid, approval_pending, approval_required
429rate_limited
502an incus_… code
503cluster_admin_not_configured, database_not_configured

GET /api/v1/provisioners/maas/machines

The machines MAAS manages, with the Apelora machine and Incus member each became. Needs cluster.view. Read-only: MAAS is asked to list, nothing else.

Operation ID: maas_machines

Needs: cluster.view.

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

curl --fail-with-body -sS -X GET "$APELORA_URL/api/v1/provisioners/maas/machines" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json"

TypeScript:

const result = await client.request("maas_machines", {});

Response 200 — Done; the response holds the result..

Content-Type: application/json.

Example response from a test installation; your IDs, names and times differ.

{
  "source": {
    "provider": "mock",
    "observedAt": "2026-10-28T23:27:11.806574Z"
  },
  "configured": false,
  "items": []
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
401unauthenticated, invalid_token
403forbidden, mfa_required
429rate_limited
502provisioner_unavailable, an incus_… code
503database_not_configured, store_unavailable

POST /api/v1/provisioners/maas/machines/{systemId}/commission

Starts commissioning. Needs hardware.commission and the hostname typed again. The cloud-init of a later deploy is not part of this. Audited; MAAS is asked first.

Operation ID: maas_commission

Needs: hardware.commission and the hostname typed again.

ParameterInTypeRequiredDefaultMeaning
systemIdpathstringyes—MAAS system id

Request body (JSON):

FieldTypeRequiredMeaning
confirmstringyesThe machine's hostname, typed again.
{
  "confirm": "rack3-04"
}

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

This request can change state. Check permissions, prerequisites and any required approval before running it.

curl --fail-with-body -sS -X POST "$APELORA_URL/api/v1/provisioners/maas/machines/4y3h7n/commission" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data-raw '{"confirm":"rack3-04"}'

TypeScript:

const result = await client.request("maas_commission", { params: { systemId: "4y3h7n" }, body: { confirm: "rack3-04" } });

Response 202 — Accepted: the work is queued; the response says how to follow it..

Content-Type: application/json.

Example response (placeholder values that show the shape):

{
  "action": "example",
  "hostname": "example",
  "systemId": "example"
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400confirmation_mismatch, invalid_request
401unauthenticated, invalid_token
403forbidden, mfa_required
404not_found
429rate_limited
500store_error
502provisioner_unavailable, an incus_… code
503provisioner_not_configured, database_not_configured

POST /api/v1/provisioners/maas/machines/{systemId}/deploy

Installs an operating system. Needs hardware.deploy and the hostname typed again. userData is sent to MAAS and is not stored or written to the audit log. Audited.

Operation ID: maas_deploy

Needs: hardware.deploy and the hostname typed again.

ParameterInTypeRequiredDefaultMeaning
systemIdpathstringyes—MAAS system id

Request body (JSON):

FieldTypeRequiredMeaning
confirmstringyes—
distroSeriesstringyesMAAS's series name, such as noble.
userDatastringyesCloud-init. Sent to MAAS and not stored. A join token in it is not kept.
{
  "confirm": "rack3-04",
  "distroSeries": "noble",
  "userData": ""
}

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

This request can change state. Check permissions, prerequisites and any required approval before running it.

curl --fail-with-body -sS -X POST "$APELORA_URL/api/v1/provisioners/maas/machines/4y3h7n/deploy" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data-raw '{"confirm":"rack3-04","distroSeries":"noble","userData":""}'

TypeScript:

const result = await client.request("maas_deploy", { params: { systemId: "4y3h7n" }, body: { confirm: "rack3-04", distroSeries: "noble", userData: "" } });

Response 202 — Accepted: the work is queued; the response says how to follow it..

Content-Type: application/json.

Example response (placeholder values that show the shape):

{
  "action": "example",
  "hostname": "example",
  "systemId": "example"
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400confirmation_mismatch, invalid_request
401unauthenticated, invalid_token
403forbidden, mfa_required
404not_found
429rate_limited
500store_error
502provisioner_unavailable, an incus_… code
503provisioner_not_configured, database_not_configured

POST /api/v1/provisioners/maas/machines/{systemId}/release

Returns the machine to MAAS's pool. Needs hardware.deploy and the hostname typed again. Disk erasure needs hardware.delete as well and is off unless asked. A secure wipe is not offered. Audited; MAAS is asked first.

Operation ID: maas_release

Needs: hardware.deploy and the hostname typed again. hardware.delete as well and is off unless asked.

ParameterInTypeRequiredDefaultMeaning
systemIdpathstringyes—MAAS system id

Request body (JSON):

FieldTypeRequiredMeaning
confirmstringyes—
eraseDisksbooleannoWipe the installed system. Off unless the caller sets it. A secure wipe is not offered.
{
  "confirm": "rack3-04",
  "eraseDisks": true
}

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

This request can change state. Check permissions, prerequisites and any required approval before running it.

curl --fail-with-body -sS -X POST "$APELORA_URL/api/v1/provisioners/maas/machines/4y3h7n/release" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data-raw '{"confirm":"rack3-04","eraseDisks":true}'

TypeScript:

const result = await client.request("maas_release", { params: { systemId: "4y3h7n" }, body: { confirm: "rack3-04", eraseDisks: true } });

Response 202 — Accepted: the work is queued; the response says how to follow it..

Content-Type: application/json.

Example response (placeholder values that show the shape):

{
  "action": "example",
  "hostname": "example",
  "systemId": "example"
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400confirmation_mismatch, invalid_request
401unauthenticated, invalid_token
403forbidden, mfa_required
404not_found
429rate_limited
500store_error
502provisioner_unavailable, an incus_… code
503provisioner_not_configured, database_not_configured

POST /api/v1/provisioners/maas/machines/{systemId}/test

Starts a hardware test run (burn-in) through MAAS. Needs hardware.commission and the hostname typed again. Only a machine that is Ready, Broken or Failed testing: one that is allocated or deployed (an Incus member, for one) is refused, so tests never load a running server. Audited with the test names; MAAS is asked first.

Operation ID: maas_test

Needs: hardware.commission and the hostname typed again.

ParameterInTypeRequiredDefaultMeaning
systemIdpathstringyes—MAAS system id

Request body (JSON):

FieldTypeRequiredMeaning
confirmstringyes—
scriptslist of stringnoMAAS test script or tag names (up to 16). Empty runs MAAS's default tests.
{
  "confirm": "rack3-04",
  "scripts": [
    "smartctl-validate"
  ]
}

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

This request can change state. Check permissions, prerequisites and any required approval before running it.

curl --fail-with-body -sS -X POST "$APELORA_URL/api/v1/provisioners/maas/machines/4y3h7n/test" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data-raw '{"confirm":"rack3-04","scripts":["smartctl-validate"]}'

TypeScript:

const result = await client.request("maas_test", { params: { systemId: "4y3h7n" }, body: { confirm: "rack3-04", scripts: ["smartctl-validate"] } });

Response 202 — Accepted: the work is queued; the response says how to follow it..

Content-Type: application/json.

Example response (placeholder values that show the shape):

{
  "action": "example",
  "hostname": "example",
  "systemId": "example"
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400confirmation_mismatch, invalid_request
401unauthenticated, invalid_token
403forbidden, mfa_required
404not_found
409machine_in_use
429rate_limited
500store_error
502provisioner_unavailable, an incus_… code
503provisioner_not_configured, database_not_configured

GET /api/v1/sectors

The sectors this control plane knows and whether each answers. Needs cluster.view.

Operation ID: sectors

Needs: cluster.view.

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

curl --fail-with-body -sS -X GET "$APELORA_URL/api/v1/sectors" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json"

TypeScript:

const result = await client.request("sectors", {});

Response 200 — Done; the response holds the result..

Content-Type: application/json.

Example response from a test installation; your IDs, names and times differ.

{
  "source": {
    "provider": "mock",
    "observedAt": "2026-10-28T23:26:50.825176Z"
  },
  "items": [
    {
      "name": "default",
      "site": "default",
      "home": true,
      "url": null,
      "state": "connected",
      "members": 3,
      "incusVersion": null,
      "credentialVars": []
    },
    {
      "name": "site-b",
      "site": "rotterdam",
      "home": false,
      "url": "https://incus.site-b.example.invalid:8443",
      "state": "connected",
      "members": 3,
      "incusVersion": null,
      "credentialVars": [
        "APELORA_SECTOR_SITE_B_CLIENT_CERT",
        "APELORA_SECTOR_SITE_B_CLIENT_KEY",
        "APELORA_SECTOR_SITE_B_SERVER_CERT"
      ],
      "addedBy": "token:docs-verifier",
      "addedAt": "2026-10-28 23:26:50.792824 +00:00:00"
    }
  ],
  "note": "Applications deploy to the home sector only. Registered sectors are read for their state and count towards the sites a second deployment could use; deploying to them is a later step."
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
401unauthenticated, invalid_token
403forbidden, mfa_required
429rate_limited
500invalid_place, store_error

POST /api/v1/sectors

Registers another Incus cluster as a sector. Stores its name, site, address and the name of its credentials; the identity's files stay on the control plane. Needs cluster.manage.

Operation ID: add_sector

Needs: cluster.manage.

Request body (JSON):

FieldTypeRequiredMeaning
credentialsstringyesNames the variables APELORA_SECTOR_<credentials>_CLIENT_CERT, _CLIENT_KEY and _SERVER_CERT on the control plane. Never a file or a secret.
namestringyes—
sitestringyes—
urlstringyeshttps://host:port of its Incus API.
{
  "name": "site-b",
  "site": "rotterdam",
  "url": "https://incus.site-b.example.invalid:8443",
  "credentials": "SITE_B"
}

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

This request can change state. Check permissions, prerequisites and any required approval before running it.

curl --fail-with-body -sS -X POST "$APELORA_URL/api/v1/sectors" \
  -H "Authorization: Bearer $APELORA_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data-raw '{"name":"site-b","site":"rotterdam","url":"https://incus.site-b.example.invalid:8443","credentials":"SITE_B"}'

TypeScript:

const result = await client.request("add_sector", { body /* the request body above */ });

Response 201 — Created; the response describes what was created..

Content-Type: application/json.

Example response from a test installation; your IDs, names and times differ.

{
  "name": "site-b",
  "site": "rotterdam",
  "home": false,
  "url": "https://incus.site-b.example.invalid:8443",
  "state": "connected",
  "members": 3,
  "incusVersion": null,
  "credentialVars": [
    "APELORA_SECTOR_SITE_B_CLIENT_CERT",
    "APELORA_SECTOR_SITE_B_CLIENT_KEY",
    "APELORA_SECTOR_SITE_B_SERVER_CERT"
  ],
  "addedBy": "token:docs-verifier",
  "addedAt": "2026-10-28 23:26:50.792824 +00:00:00"
}

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400invalid_request, invalid_sector
401unauthenticated, invalid_token
403forbidden, mfa_required
409sector_exists
429rate_limited
500invalid_place, internal
503no_database, database_not_configured, store_unavailable

DELETE /api/v1/sectors/{name}

Forgets a registered sector. Nothing changes in its Incus. Needs cluster.manage.

Operation ID: remove_sector

Needs: cluster.manage.

ParameterInTypeRequiredDefaultMeaning
namepathstringyes—Sector name
confirmquerystringyes—The sector's name, typed again.

Example request. Replace sample names, IDs and body values with resources and settings from your installation.

This request can change state. Check permissions, prerequisites and any required approval before running it.

curl --fail-with-body -sS -X DELETE "$APELORA_URL/api/v1/sectors/site-b?confirm=site-b" \
  -H "Authorization: Bearer $APELORA_TOKEN"

TypeScript:

await client.request("remove_sector", { params: { name: "site-b" }, query: { confirm: "site-b" } });

Response 204 — Done; nothing is returned., with no body.

Errors. The body is {"error": {"code": …, "message": …}}; see error codes for the codes every operation shares.

StatusCodes
400confirmation_mismatch, invalid_request
401unauthenticated, invalid_token
403forbidden, mfa_required
404not_found
429rate_limited
503no_database, database_not_configured, store_unavailable

This page is a copy of packages/api-client/openapi.json in the repository, which is the source of truth. Suggest a change.