Clipshot exposes a local HTTP API in browser mode and in daemon setups that enable the HTTP server.

Base behavior

  • Default bind: 127.0.0.1:<port>
  • Override bind: CLIPSHOT_HTTP_BIND=0.0.0.0|127.0.0.1|::1|localhost
  • JSON envelope for JSON endpoints:
{"success": true|false, "data": ..., "error": null|"message"}
  • Unknown GET routes fall back to the embedded SPA index.html
  • OPTIONS gets 204 with CORS headers

Canonical response types

DaemonStatusFull

Fields returned by GET /api/status and Tauri get_daemon_status:

  • running: bool
  • sync_enabled: bool
  • node_id: string
  • listen_addr: string
  • connected_peers: number
  • total_peers: number
  • node_name: string
  • uptime_secs: number
  • share_uri_expires_secs: number
  • iroh_enabled: bool
  • iroh_addr: string | null
  • messages_sent: number
  • messages_received: number
  • hub_connected: bool
  • hub_url: string | null
  • hub_limit_reason: string | null
  • hub_limit_current: number | null
  • hub_limit_max: number | null
  • hub_limit_manage_url: string | null
  • hub_limit_message: string | null
  • sync_state: "idle" | "sending" | "receiving" | "no_peers" | "paused"
  • app_version: string
  • tick_age_ms: number
  • peer_relay_url: string | null
  • home_relay_state: string | null"not_configured" | "connecting" | "attached"; serde-default-safe
  • relay_connecting_secs: number | null — seconds the home relay has been stuck connecting; absent when not connecting (doctor runs in a separate process, so the elapsed time is projected through status)
  • home_relay_fallback: string | null — reason a configured custom relay was stripped before endpoint creation; currently only "n0-self-relay-stripped". Absent for unconfigured nodes and for normal custom-relay operation
  • dead_workers: string[] — endpoint worker(s) that exited unexpectedly and have not yet been cleared by a deliberate respawn; empty in the normal case, serde-default-safe
  • dead_workers_detected_secs: number | null — seconds since the current dead-worker set last changed; null when no worker is reported dead
  • hub_disconnect_reason: string | null — set when the portal removed this device from its group (HubAction::Removed); null in the normal case
  • pending_remove_intents: number — pending peer-removal retry intents awaiting delivery; serde-default-safe (absent → 0)
  • remove_intent_oldest_secs: number | null — age in seconds of the oldest pending remove-intent; null when none pending. Lets doctor flag a removal that has not converged against an offline target
  • local_tombstones: number — local peer-removal tombstones currently stored; serde-default-safe (absent → 0)
  • tombstone_oldest_secs: number | null — age in seconds of the oldest local tombstone; null when none stored
  • stale_tombstoned_peers: number — peers present in the registry and still tombstoned locally (the actually-divergent, holder-side node behind “dead node in half the fleet”); serde-default-safe (absent → 0)

FullPeerInfo

Fields returned by GET /api/peers and Tauri get_peers:

  • name
  • address
  • connected
  • status: Online | Offline | Connecting
  • node_id: string | null
  • addresses: string[]
  • latency_ms: number | null
  • last_sync: number | null
  • transport_type: string | null
  • limited: bool
  • reject_reason: string | null
  • device_type: string | null
  • device_id: string | null
  • transfer_bytes_done: number | null
  • transfer_bytes_total: number | null
  • transfer_speed_bps: number | null
  • connection_type: string | null
  • path_rtt_ms: number | null
  • last_reachable_secs: number | null
  • last_indirect_secs: number | null
  • data_plane_state: string | null"ok" when connected and outbound push liveness is healthy; "degraded" when quarantined by push failures; serde-default-safe
  • consecutive_push_failures: number | null — consecutive outbound clipboard-push failures, serialized only when ≥1; serde-default-safe
  • push_attempts_since_quarantine: number | null — real outbound push attempts observed after this peer entered quarantine; serialized only when the peer is quarantined and attempts ≥1, so it distinguishes actual failed attempts from no-attempt starvation; serde-default-safe
  • consecutive_inbound_stalls: number | null — consecutive inbound drain stalls observed by ConnectionMonitor; serde-default-safe

ActivityEvent

  • id
  • event_type
  • description
  • timestamp
  • peer_name
  • content_type?
  • size_bytes?
  • error_message?
  • file_path?

TransferInfo

  • id
  • direction: "sending" | "receiving"
  • peer_name
  • peer_addr
  • content_type
  • total_bytes
  • transferred_bytes
  • status: "in_progress" | "complete" | "failed"
  • started_at
  • completed_at?
  • file_path?
  • error_message?
  • chunks_sent
  • chunks_total
  • speed_bytes_per_sec

HistoryEntry

  • id
  • entry_type
  • name
  • size_bytes
  • peer_name
  • direction
  • status
  • timestamp
  • file_path?
  • content_type
  • transferred_bytes?
  • total_bytes?
  • chunks_sent?
  • chunks_total?
  • speed_bytes_per_sec?
  • error_message?
  • completed_at?

FileTransferJob

Fields returned by GET /api/file/transfers:

  • id: u64
  • file_path: string
  • file_name: string
  • file_size: u64
  • peer_addr: string
  • peer_name: string
  • status: "queued" | "sending" | "complete" | "failed" | "cancelled"
  • transferred_bytes: u64
  • error: string | null
  • created_at: u64
  • completed_at: u64 | null

PendingInfo

Fields returned by GET /api/file/pending:

  • id: u64
  • file_name: string
  • file_size: u64
  • from_peer: string

EnvironmentStatus

Fields returned by GET /api/environment/status:

  • was_healed: bool — true if the daemon self-healed a wrong headless-on-desktop environment
  • mode: "daemon" | "gui" — the serving process UI mode

Core / SPA / static routes

Method Path Body Response
GET / embedded index.html
GET /index.html embedded index.html
GET /assets/* embedded static asset or 404 text/plain
GET /health {"status":"ok"}
GET /api/icon.png PNG chosen from current global sync state
GET /api/permissions ApiResponse<PermissionInfo[]> (macOS Input Monitoring etc.)
OPTIONS any API path 204 + CORS headers

Status / activity / share routes

Method Path Body Response
GET /api/status ApiResponse<DaemonStatusFull>
GET /api/activity ApiResponse<ActivityEvent[]>
GET /api/share_uri ApiResponse<{ uri, node_id, expires_in_secs }> or error if no routable addresses
GET /api/gossip_peers same payload as /api/peers
GET /api/transfers ApiResponse<TransferInfo[]>
GET /api/history?filter=<opt>&limit=<opt> ApiResponse<HistoryEntry[]>, default limit=100
GET /api/history/:id/content raw file bytes; image/* if history entry is image, else text/plain; charset=utf-8
GET /api/last_sync_path last sync file path

Peer routes

Method Path Body Response
GET /api/peers ApiResponse<FullPeerInfo[]>
POST /api/peers {"name":"...","address":"...","auth_code"?:"..."} queues PeerCommand::Add
PUT /api/peers {"address":"...","name"?:string,"addresses"?:string[],"auth_code"?:string|null} queues PeerCommand::Update
DELETE /api/peers/:name queues remove by peer name or stable hex node ID
POST /api/peers/:name/connect queues ConnectByName
POST /api/peers/:name/activate queues ActivateByName
POST /api/peers/:name/deactivate queues DeactivateByName
POST /api/peers/:name/reconnect queues Reconnect
POST /api/peers/reconnect_all queues a reconnect sweep for all peers
POST /api/add_peer_from_uri {"uri":"clipshot://..."} queues AddFromUri

Validation details:

  • AddPeerRequest.name: 1..64 chars
  • AddPeerRequest.address: 7..255 chars
  • AddPeerRequest.auth_code: optional string for password-protected peers
  • UpdatePeerRequest.address: 1..255 chars
  • AddPeerFromUriRequest.uri: 1..2048 chars, plus non-blank after trim

Settings routes

Method Path Body Response
GET /api/settings ApiResponse<Settings> from daemon-authoritative projection
POST /api/settings partial settings update body queues UpdateSettings
POST /api/settings/replace_group_token {"group_token":"clip_..."} intentional token replacement: store-level write bypassing the disk-preserve merge, then queues PeerCommand::ReconnectHub

Accepted update fields in POST /api/settings:

  • auto_sync (sync_enabled alias also accepted)
  • auto_start
  • notifications
  • sync_interval_ms
  • max_file_size_mb
  • listen_port
  • enable_iroh
  • max_peers
  • auto_discover
  • use_browser_ui
  • direct_send_threshold_mb
  • hotkey
  • hub_url (disk-preserved when already set — the save-path merge keeps the on-disk value)
  • group_token (disk-preserved when already set — use POST /api/settings/replace_group_token for replacement)
  • relay_url
  • max_peers_free
  • peer_relay_enabled
  • n0_relay_enabled
  • sync_max_files
  • sync_max_age_days
  • sync_max_size_mb
  • broadcast_queue_size
  • auto_accept_files
  • received_files_dir

Not accepted via HTTP API:

  • is_pro
  • node_password
  • central_relay_enabled (hub-authoritative; serde alias relay_enabled)
  • paste_path_hotkey
  • paste_shortcut
  • peer_removal_enforce (trusted peer-removal gates; settings.toml only)
  • cors_origins
  • theme
  • poll_interval_ms (use sync_interval_ms API alias instead)
  • external_dns_fallback (kill-switch for the DoH fallback layer; settings.toml only)
  • allow_self_relay (self-relay guard bypass for relay-host operators; settings.toml only)
  • diagnostic_telemetry (single reusable opt-in for Clipshot-owned diagnosis logs, default false; settable via settings.toml, a native GUI full-settings save, or CLIPSHOT_DEBUG_TELEMETRY=1 — not over HTTP, because remote enablement would amplify log volume)

Validation ranges enforced by UpdateSettingsRequest (src/http/types.rs):

  • sync_interval_ms: 100..3_600_000
  • max_file_size_mb: 1..10240
  • listen_port: 1024..65535
  • max_peers: 1..1000
  • direct_send_threshold_mb: 1..100
  • max_peers_free: 1..1000
  • sync_max_files: 0..10000 (0 = unlimited)
  • sync_max_age_days: 0..3650 (0 = unlimited)
  • sync_max_size_mb: 0..100000 (0 = unlimited)
  • broadcast_queue_size: 1..10

API ranges are wider than the Settings page inputs

The number inputs on the Settings page (web/src/pages/SettingsPage.tsx) use narrower min/max attributes than the HTTP validator. Values between the UI cap and the API ceiling are accepted by POST /api/settings but cannot be typed into the GUI:

Field Settings page input HTTP API validation Default
max_file_size_mb 1..500 1..10240 10
sync_max_files 0..10000 0..10000 100
sync_max_age_days 0..365 0..3650 30
sync_max_size_mb 0..10000 0..100000 200
max_peers 1..100 1..1000 10
poll_interval_ms / sync_interval_ms 100..10000 (step 100) 100..3_600_000 500
listen_port 1024..65535 1024..65535 19231
direct_send_threshold_mb 1..100 1..100 1
broadcast_queue_size 1..10 1..10 1

The Settings page field labels use poll_interval_ms; the HTTP API alias for the same field is sync_interval_ms. See Settings for the UI-side view of these ranges.

Sync / transfer / clipboard routes

Method Path Body Response
POST /api/retry_transfer {"file_path":"..."} queues retry by path
POST /api/copy_to_clipboard {"text":"..."} queues local clipboard write + broadcast
POST /api/sync/pause queues PauseSync
POST /api/sync/resume queues ResumeSync
POST /api/sync/toggle queues ToggleSync
POST /api/paste_path saves clipboard to sync dir, writes path, simulates paste

Validation:

  • RetryTransferRequest.file_path: 1..4096 chars
  • CopyToClipboardRequest.text: 1..1_000_000 chars

File transfer routes (Pro)

Method Path Body Response
POST /api/file/send {file_path, peer_addrs: [[addr, name]]} {job_ids}
GET /api/file/transfers FileTransferJob[]
POST /api/file/reorder {job_id, direction} {ok}
POST /api/file/cancel {job_id} {ok}
POST /api/file/accept {request_id} {path}
POST /api/file/decline {request_id} {ok}
GET /api/file/pending PendingInfo[]

No file size limits. Files <50MB use single push, ≥50MB use chunked streaming with resume. Pro gate: sending requires Pro subscription (bypass with CLIPSHOT_TEST_HOOKS=1).

Pair routes (pair-v2, src/http/handlers_pair_v2.rs)

Unified pairing API for GUI, browser UI, and CLI. Code is a 6-digit numeric string.

Method Path Body Response
POST /api/pair/generate {code, digits: null, expires_in_secs}
POST /api/pair/join {code} {digits, peer_addr, joiner_eph_pub_hex}
POST /api/pair/poll {code} {ready, joiner_eph_pub_hex, joiner_iroh_addr}
POST /api/pair/confirm {code, joiner_eph_pub_hex, joiner_iroh_addr?} {digits}
POST /api/pair/abort {code} {aborted, code}

Notes:

  • code is a numeric 6-digit string (^[0-9]{6}$)
  • generate stores an ephemeral X25519 keypair and returns digits: null (unknown until joiner arrives)
  • join performs X25519 DH, pushes joiner eph_pub to portal, adds generator as peer
  • poll is called by generator to detect joiner arrival via portal
  • confirm computes DH with joiner eph_pub, derives 4 confirmation digits, adds joiner as peer (reciprocal)
  • abort cleans up local keypair; subsequent confirm returns 404

Environment routes

Method Path Body Response
GET /api/environment/status ApiResponse<{ was_healed: bool, mode: "daemon" \| "gui" }>
POST /api/environment/relaunch-gui { spawned: true } or 500 error

Tauri command reference (src/gui/commands.rs)

The desktop GUI talks to the daemon over Tauri IPC instead of HTTP. src/gui/commands.rs defines 47 #[tauri::command] functions; 46 of them are registered in the invoke_handler list in src/gui/tauri_app.rs. toggle_clipboard_mode is defined but not registered — it is called internally from the Rust hotkey path and is not reachable from frontend IPC.

The browser UI reaches the same functionality through the HTTP routes above; see web/src/lib/api/endpoints.ts for the fallback mapping.

Status / peers

Command Params Returns
get_daemon_status DaemonStatusFull
get_peers FullPeerInfo[]
add_peer name: string, address: string, password?: string ()
remove_peer peer_id: string ()
reconnect_peer name: string ()
reconnect_all_peers ()
update_peer address: string, name?: string, addresses?: string[], auth_code?: string \| null ()
connect_to_peer peer_id: string boolean
activate_peer address: string ()
deactivate_peer address: string ()

Discovery / share

Command Params Returns
get_network_interfaces NetworkInterface[]
export_node_url node_name: string, node_id: string, interfaces: NetworkInterface[] string (signed clipshot:// URI)
add_peer_from_uri uri: string ()
discover_peers DiscoveredPeer[] (mDNS, empty when the feature is off)

Activity / settings / pairing

Command Params Returns
get_activity limit: number ActivityEvent[]
get_settings Settings
save_settings settings: Settings () (full struct required; also updates the hotkey listener)
replace_group_token token: string () (intentional token replacement + hub reconnect)
pair_generate {code, digits: null, expires_in_secs}
pair_join code: string {digits, peer_addr, joiner_eph_pub_hex}
pair_poll code: string {ready, joiner_eph_pub_hex, joiner_iroh_addr}
pair_confirm code: string, joiner_eph_pub_hex: string, joiner_iroh_addr?: string {digits}
pair_abort code: string {aborted, code}

On pair_confirm argument naming: the Rust signature is snake_case (joiner_eph_pub_hex, joiner_iroh_addrsrc/gui/commands.rs), and that is the canonical spelling. The browser HTTP fallback additionally accepts the camelCase forms as aliases — web/src/lib/api/endpoints.ts reads args?.joiner_eph_pub_hex ?? args?.joinerEphPubHex (and likewise joiner_iroh_addr ?? joinerIrohAddr) before posting to /api/pair/confirm. So callers may pass either spelling from the frontend, while the wire body and the Tauri command always use snake_case. CLAUDE.md documents the camelCase form for this command; both refer to the same parameter.

Sync / transfers / history

Command Params Returns
toggle_clipboard_mode "image" \| "path"not registered in invoke_handler, internal Rust hotkey path only
paste_file_path ()
pause_sync ()
resume_sync ()
get_transfers TransferInfo[]
get_history filter?: string, limit: number HistoryEntry[]
retry_transfer file_path: string string
copy_to_clipboard text: string () (local copy + broadcast)
copy_file_path path: string () (local clipboard only)

File transfer (Pro)

Command Params Returns
send_file file_path: string, peer_addrs: [string, string][] SendFileResponse { job_ids }
get_file_transfers FileTransferJob[]
reorder_file_transfer job_id: number, direction: string () ("up"/"down", queued jobs only)
cancel_file_transfer job_id: number ()
accept_incoming_file request_id: number AcceptIncomingFileResponse { path }
decline_incoming_file request_id: number ()
get_pending_incoming_files PendingInfo[]

OS / shell / update / permissions

Command Params Returns
open_containing_folder path: string ()
open_external_url url: string () (only http:// and https://)
restart_app ()
check_for_update string \| null
install_update ()
check_permissions PermissionInfo[]
open_permission_settings permission: string ()
restart_app_for_permissions ()

Test-only daemon HTTP routes (src/http/handlers_test.rs)

These require CLIPSHOT_TEST_HOOKS=1 or return 403.

Method Path Body Response
GET /api/test/clipboard clipboard snapshot: text returns type/data/size, binary returns type/size
POST /api/test/reset optional {"restore_peer_name"?,"restore_peer_addr"?,"max_peers_free"?} queues reset-state command
POST /api/test/kill_iroh_listener kills the active iroh listener (forces iroh restart on next tick)
POST /api/test/set_pro {"is_pro": bool} sets Pro state for testing
GET /api/test/peer_registry_dump full peer registry diagnostic snapshot
GET /api/test/reconnect_state per-peer reconnect state snapshot (failures, cooldown deadlines)
GET /api/test/removal_state peer-removal gate state snapshot
GET /api/test/transport_health transport health metrics (supervisor view)
GET /api/test/iroh_metrics full EndpointMetrics from iroh registry
GET /api/test/iroh_metrics_openmetrics OpenMetrics text format of iroh metrics
GET /api/test/connection_paths?peer=<iroh-url> per-peer connection path snapshot
GET /api/test/connection_paths_all bulk connection paths for all peers

Tauri hotkey test-hook HTTP server

This is a separate tiny_http server started from src/gui/tauri_app.rs only when CLIPSHOT_TEST_HOOKS=1.

Defaults:

  • bind: 127.0.0.1:${CLIPSHOT_TEST_HOOKS_PORT:-18181}

Routes:

Method Path Body Response
GET / POST /api/test/health daemon readiness (200 ready or 503 starting)
GET / POST /api/test/sync_enabled current sync_enabled bool
GET / POST /api/test/hotkey_metrics queue/drain latency metrics
GET /api/test/native_listener_health native hotkey listener health
GET /api/test/icon_state { state, state_raw } from global sync atomic
GET /api/test/native_key_events captured native key event snapshot
POST /api/test/native_key_events clears captured native key events
POST /api/test/hotkey_toggle optional X-Clipshot-Source header emits hotkey-toggle-sync into app event loop
GET /api/test/status daemon status snapshot
GET /api/test/peers peer list snapshot
GET /api/test/transport_health transport health metrics
POST /api/test/file/send {file_path, peer_addrs} enqueue file transfer (Pro gate)
GET /api/test/file/transfers file transfer queue snapshot
GET /api/test/file/pending pending incoming files
POST /api/test/file/cancel {job_id} cancel queued file transfer
POST /api/test/file/reorder {job_id, direction} reorder queued file transfer
POST /api/test/file/accept {request_id} accept incoming file
POST /api/test/file/decline {request_id} decline incoming file
GET / POST /api/test/settings GET: read settings; POST: partial update settings read/write for E2E
POST /api/test/set_pro {is_pro: bool} set Pro state for testing

Portal-side endpoints (not local daemon)

The following routes are served by the Clipshot Portal (clipshot.cc), not by the local daemon HTTP API. They are listed here for completeness.

Setup sessions (device auth flow)

Method Path Body Response
POST /api/setup/sessions { session_id, url, expires_in }
GET /api/setup/sessions/:id { status: "pending"\|"completed"\|"expired", group_token? }
POST /api/setup/sessions/:id/complete { group_token } (requires JWT) 200

Used by curl | bash install script and GUI “Create Account” button. Session expires in 10 minutes.

OAuth

Method Path Notes
GET /api/auth/google Redirect to Google OAuth. Optional ?state=SESSION_ID for device auth.
GET /api/auth/google/callback Google callback — creates/finds user, issues JWT.
GET /api/auth/github Redirect to GitHub OAuth.
GET /api/auth/github/callback GitHub callback.

Back to top

Clipshot — P2P clipboard sync across your devices.

This site uses Just the Docs, a documentation theme for Jekyll.