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>: for examplemount_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 | none |
remote |
string | yes | none |
local |
string | yes | none |
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 | none |
source |
string | yes | none |
target |
string | yes | none |
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 |