Skip to content

API

Borg UI exposes authenticated API routes under /api. This page focuses on the manual backup flow that automation clients commonly need: get an access token, start an existing repository backup, then poll status and logs.

The examples use X-Borg-Authorization because Borg UI gives that header precedence when both headers are present. The legacy Authorization: Bearer TOKEN header is still accepted for compatibility.

Create an access token

For local username/password auth, create a short-lived bearer token with the login endpoint:

bash
BASE_URL="https://backups.example.com"

TOKEN="$(
  curl -sS -X POST "$BASE_URL/api/auth/login" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    --data-urlencode "username=admin" \
    --data-urlencode "password=change-this-password" |
    jq -r .access_token
)"

SSO, TOTP, and passkey deployments may require their normal interactive login flow to produce an accepted bearer token. Generated tokens from Settings > Account are shown once and can be revoked there, but manual API requests still require the bearer token from a normal login. Do not use generated borgui_... account tokens as standalone credentials for manual backup endpoints yet.

The token inherits the signed-in user's permissions:

  • starting or cancelling a backup requires operator access to the repository
  • polling status, streaming logs, or downloading logs requires viewer access to the repository
  • admins have access to every repository

Start a manual backup

Start an existing repository backup with POST /api/backup/start:

bash
REPOSITORY="/backups/server1"

START_RESPONSE="$(
  curl -sS -X POST "$BASE_URL/api/backup/start" \
    -H "X-Borg-Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d "{\"repository\":\"$REPOSITORY\"}"
)"

JOB_ID="$(printf '%s\n' "$START_RESPONSE" | jq -r .job_id)"
printf '%s\n' "$START_RESPONSE"

Successful responses use this shape:

json
{
  "job_id": 123,
  "status": "pending",
  "message": "Backup job started"
}

The job_id is an operations id. The same number addresses GET /api/operations/{id} and the Activity log routes with job type backup. A job stays pending while another exclusive operation holds the repository, and POST /api/backup/cancel/{id} cancels a pending job as well as a running one.

The JSON body uses the repository string accepted by Borg UI's manual backup flow. For the current /api/backup/start and /api/backup/run endpoints, pass the repository path shown in Borg UI. Older clients may still submit requests without a registered repository path. For compatibility, those requests can be accepted, but unknown paths fail to authorize or route. Automation should send a registered repository path so permissions, routing, and logs resolve against the intended repository.

Compatibility alias

POST /api/backup/run is a compatibility alias for clients that use run instead of start. It accepts the same request body and returns the same response shape:

bash
curl -sS -X POST "$BASE_URL/api/backup/run" \
  -H "X-Borg-Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"repository\":\"$REPOSITORY\"}"

Poll job status

After receiving job_id, poll /api/backup/status/{job_id} until the job reaches a terminal status:

bash
curl -sS "$BASE_URL/api/backup/status/$JOB_ID" \
  -H "X-Borg-Authorization: Bearer $TOKEN"

The status payload includes the repository, current status, timestamps, progress, error details, logs summary, and progress details:

json
{
  "id": 123,
  "repository": "/backups/server1",
  "status": "running",
  "started_at": "2026-06-04T17:30:00+00:00",
  "completed_at": null,
  "progress": 42.0,
  "error_message": null,
  "logs": null,
  "progress_details": {
    "progress_percent": 42.0,
    "current_file": "/srv/app.db"
  }
}

Common statuses include pending, running, completed, completed_with_warnings, failed, and cancelled.

Poll job logs

Use /api/backup/logs/{job_id}/stream for incremental log polling. Start with offset=0; for later calls, use the previous total_lines value as the next offset.

bash
curl -sS "$BASE_URL/api/backup/logs/$JOB_ID/stream?offset=0" \
  -H "X-Borg-Authorization: Bearer $TOKEN"

The log stream response is line-oriented:

json
{
  "job_id": 123,
  "status": "running",
  "lines": [
    {
      "line_number": 1,
      "content": "Starting backup"
    }
  ],
  "total_lines": 1,
  "has_more": false
}

For completed non-running jobs with stored logs, you can also download a text file:

bash
curl -sS "$BASE_URL/api/backup/logs/$JOB_ID/download" \
  -H "X-Borg-Authorization: Bearer $TOKEN" \
  -o "backup_job_${JOB_ID}_logs.txt"

Maintenance jobs (check, prune, compact, restore check, archive delete)

The start routes (POST /api/repositories/{id}/check, /prune, /compact, /restore-check, and DELETE /api/archives/{archive_id}) return {"job_id", "status", "message"}, where job_id is an operations row id: the work is queued behind the repository's other exclusive operations (a running backup, for example) instead of being rejected with a conflict, so status is usually pending rather than running. A prune's dry run is the exception: it runs and answers inline, so its payload carries the prune_result shape instead of a job_id to poll.

The corresponding status and list routes are GET .../check-jobs/{id}, /prune-jobs/{id}, /compact-jobs/{id}, /restore-check-jobs/{id}, /api/archives/delete-jobs/{id}, and their per-repository list forms.

Repository wipe, cloud mirror, and package install jobs

These three kinds are operations rows too, and every response body and status word is unchanged.

POST /api/repositories/{id}/wipe still answers {"id", "status", "phase", ...} with status: "pending", but the wipe is now queued behind the repository's other exclusive work rather than started immediately, and GET /api/repositories/{id}/wipe-jobs/{job_id} polls it as before. The preview from POST .../wipe-preview keeps its own id space in repository_wipe_jobs, the one table that still holds previews; both id spaces resolve on the status and cancel routes. The statuses completed_compaction_failed and failed_partial are still returned, reconstructed from the operation's wipe details.

The cloud mirror latest_sync_job block on the repository payload keeps its shape, including triggered_by: "initial" for the sync queued when a cloud repository is created, and operation: "sync" | "hydrate". Activity still reports the two as types rclone_sync and rclone_hydrate.

POST /api/packages/{id}/install and GET /api/packages/jobs/{job_id} keep their bodies, including stdout, stderr, and exit_code: the two streams now live in the operation's log file and are parsed back for the response. The install is queued and started by the runner, so a fresh job answers pending before it answers installing.

Restore jobs

A restore is an operations row as well. POST /api/restore/start answers {"job_id", "status": "pending", "message"}, where the id is an operations row id. GET /api/restore/jobs, GET /api/restore/status/{id}, and POST /api/restore/cancel/{id} keep their bodies and status words. progress_details.estimated_time_remaining is computed from the sizes and the speed rather than stored. The restore's logs are its operation log file, readable through GET /api/activity/restore/{id}/logs as before.

Operations

Every job is an operations row, and these routes read and steer them directly. Each job_id the job routes return is an operation id usable here.

RoutePurpose
GET /api/operations/List operations, filtered by repository_id, category[], kind[], status[], trigger[], run_id, since, with limit and a cursor for paging
GET /api/operations/queueWhat is running and waiting right now, with the concurrency limits in force
GET /api/operations/repositoriesOne row per repository the user may see, with the state of its derived data
GET /api/operations/repositories/{id}The archives behind a row's failed and truncated counts, newest first
POST /api/operations/reconcileRun the reconcile tick now instead of waiting for the interval
POST /api/operations/pauseStop dispatching follow-up and reconcile work
POST /api/operations/resumeDispatch it again
PUT /api/operations/limitsChange the concurrency limits
GET /api/operations/{id}One operation with its kind-specific detail
POST /api/operations/{id}/cancelAsk the runner to cancel it
GET /api/operations/{id}/logsPaginated log lines
GET /api/operations/{id}/logs/downloadThe log file as a download

Activity

GET /api/activity/recent is the unified history. Parameters: limit, job_type, status, category[], trigger[], repository_id, and collapse_runs (on by default, which nests a run's index follow-ups under their parent).

Each item carries activity_key, type, category, trigger, followups, and the fields the job views have always read: id, status, started_at, completed_at, error_message, repository, repository_path, log_file_path, triggered_by, schedule_id, archive_name, package_name, and has_logs.

Three routes serve one item by type and id: GET /api/activity/{job_type}/{id}/logs, GET /api/activity/{job_type}/{id}/logs/download, and DELETE /api/activity/{job_type}/{id}. The job_type values are the Activity type words (backup, restore, check, restore_check, compact, prune, package, rclone_sync, rclone_hydrate, script_execution).

Archive index and history

Database-backed archive routes under /api/repositories/{id}. Routes marked Pro require the archive_history feature and return the standard plan 403 payload otherwise.

MethodRoutePlanPurpose
GET/archivesCommunityArchives from the index with series, since, until filters and sync_state
GET/archives/liveCommunityThe previous live borg list route, kept for the Archives page until it switches to the index. Also served by the v2 router at /api/v2/repositories/{id}/archives/live, so one client reaches the live listing on both borg versions
GET/archives/heatmapCommunityPer day counts and sizes for the whole repository and per series; missed_run days; outlier flags on Pro
GET/archives/{archive_id}CommunityOne archive with history state and neighbours
GET/statusCommunityPer-category status from repository evidence (newest archive, detected removals, job rows); overdue flags on Pro. The repositories list payload carries last_prune and last_index from the same evidence for the card
POST/rebuildCommunity (history stage is Pro)Body {"from": "stats" | "archives" | "history"}. Answers with index_mode and repeats
POST/resyncCommunityBrings the stored archive list back in line after work that removed archives. Answers with index_mode and repeats
GET/archives/{archive_id}/changesProChanges against the predecessor or compare_to. incomplete and unindexed_archive_ids flag a fold whose window contains an archive that was never indexed
GET/history?path=ProEvery archive that touched a path, with present ranges
GET/search?q=ProFilename search across all archives

GET /api/archives/list is deprecated and sends Deprecation: true with a Link header pointing at the index route.

since and until accept an offset (2026-01-01T00:00:00Z); the value is converted to UTC before it reaches the index, which stores naive UTC.

Index mode

Every repository payload carries index_mode, one of full (the default), archives or off. It says how much derived data the repository keeps refreshed: full indexes everything, archives keeps the archive list and the size current but builds no file history, and off refreshes nothing.

PUT /api/repositories/{id} accepts index_mode; any other value is a 422. Changing it away from full cancels the repository's queued index work (a running index is left to finish), and changing it back to full enqueues one catch-up run.

Manual work is not blocked by the mode. /rebuild and /resync run once for a repository in any mode. repeats is false as soon as any stage the call asked for will not be kept fresh afterwards: every stage for off, and the history stages for archives. Neither route builds file history for a mode that excludes it. repositories.history_index_excludes, the glob patterns file history skips, is accepted on the same route.

Distributed under the AGPL-3.0 License.