Base URL: http://<host>:<port> (default http://0.0.0.0:8765).
When api.api_key is configured, all write endpoints (POST, PUT, DELETE, PATCH) require:
X-API-Key: <value>
Read endpoints (GET) are unauthenticated — _check_api_key — api.py:111.
Missing or incorrect key returns HTTP 401.
Returns the HTML dashboard. Served by Jinja2; not suitable for API clients.
Returns 200 if all enabled mounts are healthy (or mounting/disabled). Returns 503 if any enabled mount is in error, unmounted, or stale — health_check — api.py:242.
200 response:
{"ok": true}503 response:
{"ok": false, "unhealthy": ["nas", "backup"]}Returns daemon version string — api_version — api.py:190.
{"version": "0.1.0"}Prometheus exposition format (text/plain; version=0.0.4) — prometheus_metrics — api.py:196.
Metrics (all with {name="<mount_name>"} labels):
| Metric | Type | Description |
|---|---|---|
sshfs_keeper_mount_healthy |
gauge | 1 if healthy, 0 otherwise |
sshfs_keeper_mount_count |
counter | Total successful mounts since start |
sshfs_keeper_mount_retry_count |
gauge | Current consecutive retry count |
sshfs_keeper_mount_duration_seconds |
gauge | Last mount operation duration |
sshfs_keeper_sync_run_count |
counter | Total sync job runs since start |
sshfs_keeper_sync_fail_count |
gauge | Consecutive failure count (resets on success) |
sshfs_keeper_sync_bytes_sent |
gauge | Bytes sent in last successful run |
sshfs_keeper_sync_last_duration_seconds |
gauge | Duration of last sync run |
Generated by metrics.generate — metrics.py:24.
Server-Sent Events stream. Each event is a JSON object on the data: line — sse_events — api.py:204.
The stream emits two SSE events per status change:
event: <event_type>— e.g.mount_healthy,mount_errorevent: mount_update_<name>— per-mount event for HTMX out-of-band swap
A : keepalive comment is sent every 30 seconds when there is no activity.
Event payload:
{
"event": "mount_healthy",
"mount": "nas",
"status": "healthy",
"timestamp": 1711234567.123
}Returns snapshot of all mount states — api_status — api.py:235.
{
"mounts": [
{
"name": "nas",
"remote": "user@host:/path",
"local": "/mnt/nas",
"enabled": true,
"mount_tool": "sshfs",
"status": "healthy",
"last_check": 1711234567.0,
"last_mounted": 1711234500.0,
"last_error": null,
"retry_count": 0,
"mount_count": 3,
"backoff_remaining": 0.0,
"mount_duration_seconds": 1.23,
"usage": {
"total_gb": 500.0,
"used_gb": 200.0,
"free_gb": 300.0,
"percent_used": 40.0
}
}
],
"timestamp": 1711234567.123
}status values: healthy, unmounted, stale, mounting, disabled, error — MountStatus — monitor.py:16.
usage is populated only when status is healthy; otherwise null — Monitor.get_snapshot — monitor.py:102.
Create a new mount. Returns 409 if name already exists — api_add_mount — api.py:346.
Request body:
| Field | Type | Required | Default |
|---|---|---|---|
name |
string | yes | — |
remote |
string | yes | — |
local |
string | yes | — |
options |
string | no | "cache=yes,compression=yes,ServerAliveInterval=15,ServerAliveCountMax=3,reconnect" |
identity |
string | null | no | null |
enabled |
bool | no | true |
mount_tool |
string | no | "sshfs" |
Response:
{"name": "nas", "created": true}Update an existing mount. Rename is supported by setting a different name in the body; returns 409 if the new name conflicts — api_update_mount — api.py:370.
Same body fields as POST /api/mounts.
Response:
{"name": "nas", "updated": true}Remove a mount config from memory and disk. Does not unmount — api_delete_mount — api.py:402.
Response:
{"name": "nas", "deleted": true}Resets backoff and retry counter, then triggers an immediate remount attempt — api_remount — api.py:418.
Response:
{"name": "nas", "success": true}Force-unmounts using fusermount3 -uz (Linux) or umount -f (macOS) — api_unmount — api.py:430.
Response:
{"name": "nas", "success": true}Sets enabled = true and saves config — api_enable — api.py:443.
Response:
{"name": "nas", "enabled": true}Sets enabled = false and saves config — api_disable — api.py:455.
Response:
{"name": "nas", "enabled": false}Toggles mount_tool between sshfs and rclone — api_switch_backend — api.py:467.
Response:
{"name": "nas", "mount_tool": "rclone"}Update daemon settings in-memory and persist to disk — api_update_settings — api.py:494.
Request body (all fields optional; only provided fields are updated):
| Field | Type |
|---|---|
check_interval |
int |
remount_delay |
int |
max_retries |
int |
backoff_base |
int |
log_level |
string |
json_logs |
bool |
Response:
{"updated": true}Returns current notification settings — api_get_notifications — api.py:516.
{
"webhook_url": "https://ntfy.sh/my-topic",
"on_failure": true,
"on_recovery": true,
"on_backoff": false
}Update notification settings and save config — api_update_notifications — api.py:528.
Request body:
| Field | Type | Default |
|---|---|---|
webhook_url |
string | null | null |
on_failure |
bool | true |
on_recovery |
bool | true |
on_backoff |
bool | false |
Response:
{"updated": true}Keys are stored in ~/.config/sshfs-keeper/keys/ at mode 600 — api_upload_key — api.py:556.
List stored private key filenames — api_list_keys — api.py:551.
{"keys": ["id_ed25519", "id_rsa"]}Upload a private key file (multipart/form-data, field name file). File must begin with PRIVATE KEY or OPENSSH — api_upload_key — api.py:556.
Response:
{"name": "id_ed25519", "path": "/home/user/.config/sshfs-keeper/keys/id_ed25519"}Delete a stored key — api_delete_key — api.py:580.
Response:
{"name": "id_ed25519", "deleted": true}Returns snapshot of all sync job states — api_list_syncs — api.py:597.
{
"syncs": [
{
"name": "backup",
"source": "/data/",
"target": "user@host:/backup/",
"interval": 3600,
"options": "-az --delete --stats",
"enabled": true,
"status": "ok",
"last_run": 1711234000.0,
"last_duration": 12.3,
"last_error": null,
"bytes_sent": 102400,
"files_transferred": 5,
"run_count": 10,
"fail_count": 0,
"sync_tool": "rsync",
"next_run_in": 3543.7
}
]
}status values: idle, running, ok, failed, disabled — SyncStatus — sync.py:22.
Create a sync job — api_add_sync — api.py:601.
Request body:
| Field | Type | Required | Default |
|---|---|---|---|
name |
string | yes | — |
source |
string | yes | — |
target |
string | yes | — |
interval |
int | no | 3600 |
options |
string | no | "-az --delete --stats" |
identity |
string | null | no | null |
enabled |
bool | no | true |
sync_tool |
string | no | "rsync" |
Response:
{"name": "backup", "created": true}Update a sync job. Rename supported — api_update_sync — api.py:620.
Same body as POST /api/syncs.
Response:
{"name": "backup", "updated": true}Remove a sync job — api_delete_sync — api.py:649.
Response:
{"name": "backup", "deleted": true}Run the sync job immediately (sets _next_run = 0) — api_trigger_sync — api.py:663.
Response:
{"name": "backup", "triggered": true}Returns the last 50 lines of stdout+stderr from the most recent sync run — api_sync_log — api.py:673.
When called with HX-Request header, returns plain HTML text. Otherwise returns JSON:
{"name": "backup", "lines": ["sent 1024 bytes ...", "..."]}Toggle enabled state and save config — api_enable_sync — api.py:695, api_disable_sync — api.py:707.
{"name": "backup", "enabled": true}These endpoints return HTML partials for in-place dashboard refresh. They are consumed by the dashboard and not intended for API clients.
| Path | Handler | Template |
|---|---|---|
GET /fragments/mounts |
fragment_mounts — api.py:263 |
_mount_cards.html |
GET /fragments/syncs |
fragment_syncs — api.py:277 |
_sync_cards.html |
GET /fragments/mounts/{name} |
fragment_mount_card — api.py:288 |
_mount_card.html |
GET /fragments/syncs/{name} |
fragment_sync_card — api.py:309 |
_sync_card.html |
GET /fragments/keys |
fragment_keys — api.py:331 |
_keys_list.html |