# Volume backups API

14 operations under /api/v1. Errors are RFC 9457 problem+json with a stable `code` field. The complete schemas are in https://nebulactrl.com/api/v1/openapi.json.

## DELETE /retired-volumes/{retiredId}

Delete a retired Docker volume

Enqueues the removal on the server; the row disappears once the agent confirmed. 409 `server_offline` while the server is offline.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `retiredId` (path, string, required)

Response 202: Accepted
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict (`server_offline`); 422 Unprocessable Entity; 500 Internal Server Error

## GET /services/{serviceId}/retired-volumes

List the Docker volumes a restore swapped out

Retired volumes are unmounted but kept on the server (their data is the rollback path of a restore) until deleted.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serviceId` (path, string, required)

Response 200: OK (RetiredVolumeListOutputBody)
  - `items` (RetiredVolumeView[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## GET /services/{serviceId}/volume-backup

A service's volume backup configuration

Always 200: `configured: false` with the defaults when none is saved. `eligible: false` (with `ineligibleReason`) for services that cannot use volume backups: managed databases (use database backups), route services and services without volumes.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serviceId` (path, string, required)

Response 200: OK (VolumeBackupConfigView)
  - `configured` (boolean, required)
  - `cron` (string, required)
  - `destinationId` (string)
  - `destinationKind` (enum: s3 | local)
  - `destinationName` (string)
  - `eligible` (boolean, required)
  - `enabled` (boolean, required)
  - `excludes` (string[] | null, required)
  - `freezeBudgetSeconds` (integer (int64), required)
  - `ineligibleReason` (string)
  - `keepDaily` (integer (int64), required)
  - `keepMonthly` (integer (int64), required)
  - `keepWeekly` (integer (int64), required)
  - `lastRunAt` (string (date-time))
  - `mode` (enum: freeze | stop | live, required)
  - `nextRunAt` (string (date-time))
  - `postHook` (VolumeBackupHook)
  - `preHook` (VolumeBackupHook)
  - `repoReady` (boolean, required): A repository password exists for this destination.
  - `serviceId` (string, required)
  - `timezone` (string, required)
  - `volumes` (VolumeBackupVolumeRef[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## PUT /services/{serviceId}/volume-backup

Save a service's volume backup configuration

Destination, mode, hooks, excludes, retention and the optional schedule. 422 `volume_backup_unavailable` for ineligible services, `invalid_cron` for a bad schedule. The first backup creates the repository.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serviceId` (path, string, required)

Request body (application/json, required): VolumeBackupConfigInput
  - `cron` (string): 5-field cron expression or @hourly/@daily/@weekly/@monthly/@yearly; empty = manual backups only. At most one run every 5 minutes.
  - `destinationId` (string, required)
  - `enabled` (boolean): Run on the schedule (needs a cron).
  - `excludes` (string[] | null): restic --exclude patterns relative to /vol, e.g. data/cache/**.
  - `freezeBudgetSeconds` (integer (int64)): Longest pause of mode freeze (default 5). Over budget the copy is abandoned and the container thawed.
  - `keepDaily` (integer (int64)): Keep the newest backup of each of the last N days (default 7, 0 = no limit).
  - `keepMonthly` (integer (int64)): Default 6, 0 = no limit. With all three at 0 nothing is ever removed.
  - `keepWeekly` (integer (int64)): Default 4, 0 = no limit.
  - `mode` (enum: freeze | stop | live): freeze pauses the container while the local copy is taken (default); stop stops it; live copies nothing and may be inconsistent.
  - `postHook` (VolumeBackupHook)
  - `preHook` (VolumeBackupHook)
  - `timezone` (string): IANA zone, default UTC.

Response 200: OK (VolumeBackupConfigView)
  - `configured` (boolean, required)
  - `cron` (string, required)
  - `destinationId` (string)
  - `destinationKind` (enum: s3 | local)
  - `destinationName` (string)
  - `eligible` (boolean, required)
  - `enabled` (boolean, required)
  - `excludes` (string[] | null, required)
  - `freezeBudgetSeconds` (integer (int64), required)
  - `ineligibleReason` (string)
  - `keepDaily` (integer (int64), required)
  - `keepMonthly` (integer (int64), required)
  - `keepWeekly` (integer (int64), required)
  - `lastRunAt` (string (date-time))
  - `mode` (enum: freeze | stop | live, required)
  - `nextRunAt` (string (date-time))
  - `postHook` (VolumeBackupHook)
  - `preHook` (VolumeBackupHook)
  - `repoReady` (boolean, required): A repository password exists for this destination.
  - `serviceId` (string, required)
  - `timezone` (string, required)
  - `volumes` (VolumeBackupVolumeRef[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity (`volume_backup_unavailable`); 500 Internal Server Error

## DELETE /services/{serviceId}/volume-backup

Remove a service's volume backup configuration

Turns the schedule off. Existing backups and the repository are kept.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serviceId` (path, string, required)

Response 204: No Content
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## POST /services/{serviceId}/volume-backup/recovery-key

Reveal the repository password

The restic repository and password, so a backup can be restored without NebulaCtrl. Requires the admin scope; audited as `volume_backup.recovery_key.reveal`. 422 `volume_backup_unavailable` until the first backup created the repository.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serviceId` (path, string, required)

Request body (application/json, required): RecoveryKeyInputBody

Response 200: OK (VolumeBackupRecoveryKey)
  - `note` (string, required)
  - `password` (string, required)
  - `repository` (string, required): The restic repository URL or path as the agent uses it.
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity (`volume_backup_unavailable`); 500 Internal Server Error

## GET /services/{serviceId}/volume-backups

List a service's volume backups

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serviceId` (path, string, required)
- `limit` (query, integer (int64))

Response 200: OK (VolumeBackupListOutputBody)
  - `items` (VolumeBackupView[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## POST /services/{serviceId}/volume-backups

Back up a service's volumes now

Needs a saved configuration and an online server. Returns 202 with a queued backup; poll it or listen for `backup.updated` events. 409 `server_offline` when it can not start; 422 `volume_backup_unavailable` for ineligible services or without a configuration.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `Idempotency-Key` (header, string): Client-chosen key (<=255 chars). A retry with the same key replays the original response for 24 hours.
- `serviceId` (path, string, required)

Request body (application/json, required): CreateVolumeBackupInputBody

Response 202: Accepted (VolumeBackupView)
  - `actor` (string)
  - `bytesAdded` (integer (int64), required)
  - `consistency` (string): The mode the agent actually used (a stopped service is backed up without a freeze).
  - `createdAt` (string (date-time), required)
  - `destinationId` (string, required)
  - `destinationKind` (string)
  - `destinationName` (string)
  - `error` (string)
  - `files` (integer (int64), required)
  - `finishedAt` (string (date-time))
  - `freezeMs` (integer (int64), required): How long the container was paused or stopped.
  - `hookOutcome` (string)
  - `id` (string, required)
  - `locked` (boolean, required)
  - `mode` (string, required): The requested mode.
  - `objectDeleted` (boolean, required): True once retention or a user removed the snapshot; the row stays as history.
  - `restorable` (boolean, required)
  - `serverId` (string)
  - `serviceId` (string, required)
  - `sizeBytes` (integer (int64), required)
  - `snapshotId` (string)
  - `stageMethod` (enum: reflink | copy | none)
  - `stageMs` (integer (int64), required): How long the local copy took.
  - `startedAt` (string (date-time))
  - `status` (enum: queued | running | succeeded | failed | skipped | expired, required)
  - `trigger` (enum: manual | schedule | pre_restore, required)
  - `volumes` (string[] | null, required)
  - `warnings` (string[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict (`server_offline`); 422 Unprocessable Entity (`volume_backup_unavailable`); 500 Internal Server Error

## GET /services/{serviceId}/volume-restores

List a service's volume restores

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serviceId` (path, string, required)

Response 200: OK (VolumeRestoreListOutputBody)
  - `items` (VolumeRestoreView[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## GET /volume-backups/{backupId}

Get a volume backup

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `backupId` (path, string, required)

Response 200: OK (VolumeBackupView)
  - `actor` (string)
  - `bytesAdded` (integer (int64), required)
  - `consistency` (string): The mode the agent actually used (a stopped service is backed up without a freeze).
  - `createdAt` (string (date-time), required)
  - `destinationId` (string, required)
  - `destinationKind` (string)
  - `destinationName` (string)
  - `error` (string)
  - `files` (integer (int64), required)
  - `finishedAt` (string (date-time))
  - `freezeMs` (integer (int64), required): How long the container was paused or stopped.
  - `hookOutcome` (string)
  - `id` (string, required)
  - `locked` (boolean, required)
  - `mode` (string, required): The requested mode.
  - `objectDeleted` (boolean, required): True once retention or a user removed the snapshot; the row stays as history.
  - `restorable` (boolean, required)
  - `serverId` (string)
  - `serviceId` (string, required)
  - `sizeBytes` (integer (int64), required)
  - `snapshotId` (string)
  - `stageMethod` (enum: reflink | copy | none)
  - `stageMs` (integer (int64), required): How long the local copy took.
  - `startedAt` (string (date-time))
  - `status` (enum: queued | running | succeeded | failed | skipped | expired, required)
  - `trigger` (enum: manual | schedule | pre_restore, required)
  - `volumes` (string[] | null, required)
  - `warnings` (string[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## DELETE /volume-backups/{backupId}

Delete a volume backup

Removes the snapshot through the agent. 409 `backup_locked`, `backup_in_progress` or `server_offline`.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `backupId` (path, string, required)

Response 204: No Content
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict (`backup_locked`); 422 Unprocessable Entity; 500 Internal Server Error

## GET /volume-backups/{backupId}/files

Browse the files of a volume backup

Lists one directory of one volume inside the snapshot (at most 5000 entries). The agent reads the repository, so the server must be online (409 `server_offline`). Requires the `deployments:write` scope: backups can contain secrets.

**Download.** `GET /volume-backups/{backupId}/files/content?volume=&path=&archive=` streams a file (`Content-Disposition: attachment`); a directory is downloaded as a `.zip` with `archive=true`. Audited as `volume_backup.file.download`.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `backupId` (path, string, required)
- `volume` (query, string): The volume's name in the service.
- `path` (query, string): Absolute directory path inside the volume.

Response 200: OK (VolumeFileListing)
  - `backupId` (string, required)
  - `entries` (VolumeFileEntry[] | null, required)
  - `path` (string, required)
  - `truncated` (boolean, required)
  - `volume` (string, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict (`server_offline`); 422 Unprocessable Entity; 500 Internal Server Error

## PUT /volume-backups/{backupId}/lock

Lock or unlock a volume backup

Locked backups are never removed by retention or deleted.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `backupId` (path, string, required)

Request body (application/json, required): LockVolumeBackupInputBody
  - `locked` (boolean, required)

Response 200: OK (VolumeBackupView)
  - `actor` (string)
  - `bytesAdded` (integer (int64), required)
  - `consistency` (string): The mode the agent actually used (a stopped service is backed up without a freeze).
  - `createdAt` (string (date-time), required)
  - `destinationId` (string, required)
  - `destinationKind` (string)
  - `destinationName` (string)
  - `error` (string)
  - `files` (integer (int64), required)
  - `finishedAt` (string (date-time))
  - `freezeMs` (integer (int64), required): How long the container was paused or stopped.
  - `hookOutcome` (string)
  - `id` (string, required)
  - `locked` (boolean, required)
  - `mode` (string, required): The requested mode.
  - `objectDeleted` (boolean, required): True once retention or a user removed the snapshot; the row stays as history.
  - `restorable` (boolean, required)
  - `serverId` (string)
  - `serviceId` (string, required)
  - `sizeBytes` (integer (int64), required)
  - `snapshotId` (string)
  - `stageMethod` (enum: reflink | copy | none)
  - `stageMs` (integer (int64), required): How long the local copy took.
  - `startedAt` (string (date-time))
  - `status` (enum: queued | running | succeeded | failed | skipped | expired, required)
  - `trigger` (enum: manual | schedule | pre_restore, required)
  - `volumes` (string[] | null, required)
  - `warnings` (string[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## POST /volume-backups/{backupId}/restore

Restore a volume of a backup

mode `new_volume` (default) restores into a new Docker volume and stages the swap on the service: apply it with Deploy; the current volume is kept as a retired volume. mode `in_place` first takes a safety backup, then stops the service, overwrites the volume and starts it again; it needs `confirmSlug`. 409 `backup_not_restorable`, `server_offline`, `backup_in_progress`; 422 `confirmation_required`.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `Idempotency-Key` (header, string): Client-chosen key (<=255 chars). A retry with the same key replays the original response for 24 hours.
- `backupId` (path, string, required)

Request body (application/json, required): RestoreVolumeBackupInputBody
  - `confirmSlug` (string): In-place restores: the service's slug.
  - `mode` (enum: new_volume | in_place): new_volume restores into a new Docker volume and stages the swap (apply it with Deploy); the current volume is kept. in_place stops the service, overwrites the volume after a safety backup and starts it again.
  - `volume` (string, required): The volume's name in the service.

Response 202: Accepted (VolumeRestoreView)
  - `actor` (string)
  - `backupId` (string, required)
  - `createdAt` (string (date-time), required)
  - `dockerName` (string)
  - `error` (string)
  - `finishedAt` (string (date-time))
  - `id` (string, required)
  - `mode` (enum: new_volume | in_place, required)
  - `preBackupId` (string)
  - `serviceId` (string, required)
  - `status` (enum: pre_backup | restoring | succeeded | failed, required)
  - `swapStaged` (boolean, required): A restore into a new volume succeeded and the swap is staged: apply it with Deploy.
  - `volumeName` (string, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict (`backup_not_restorable`); 422 Unprocessable Entity (`confirmation_required`); 500 Internal Server Error
