Mobile LAN API
SyncClipboard-compatible HTTP endpoints exposed by the desktop daemon for mobile clients.
The desktop daemon exposes a small SyncClipboard-compatible HTTP service on the LAN so a paired mobile device — iOS Shortcuts, Android, or any third-party SyncClipboard client — can read and push clipboard items.
All endpoints are mounted at the root path. The main SyncClipboard paths are
/SyncClipboard.json and /file/{dataName}; Android-compatible clients may also
call /api/history/*, /api/time, /api/version, /version, /, and /file.
There are also two UniClipboard private extensions. Neither is part of the
SyncClipboard protocol, and third-party clients can ignore both:
GET /api/sse/clipboard(Server-Sent Events): pushes a signal to subscribed clients whenever the desktop clipboard changes; the client then pullsGET /SyncClipboard.json, replacing periodic polling.GET /healthz: a constant-cost liveness probe, so a client that has gone offline can discover when the listener is reachable again.
Every endpoint requires HTTP Basic Authentication except GET /healthz.
Credentials are minted on the desktop side during pairing and are bound to a
single registered device.
GET /healthz is public by design: it returns an empty 204 with no version,
device, username, or clipboard information — revealing only that a listener
answers at this address, which a TCP connect already reveals. Requiring
credentials would instead mean running an Argon2id verification on every probe,
which is the abuse vector rather than a defense against it.
This API is the mobile companion path. It is independent of the iroh P2P transport that desktop ↔ desktop sync uses, runs on plain HTTP, and intentionally has no NAT hole-punching and no encrypted relay of its own. Off-LAN clients can't reach it directly — front it with a headless server node (public HTTPS) or a Tailscale / VPN overlay to reach it across networks. See Pairing & sync — Mobile companion for the trade-offs.
For setup, see Pairing & sync — Mobile companion and the Devices → Mobile sync panel in the desktop app.
Compatibility boundary
UniClipboard exposes the subset of the SyncClipboard server API needed for current clipboard sync and Android client compatibility. It is not a full SyncClipboard history server.
| Endpoint group | Status |
|---|---|
GET /SyncClipboard.json, PUT /SyncClipboard.json | Real clipboard metadata read/write for the latest item. |
GET /file/{dataName}, PUT /file/{dataName}, HEAD /file/{dataName} | Real binary download/upload for image and file items. |
POST /api/history | Real Android-compatible inbound path for text, image, and file uploads. |
GET /api/history/{profileId}, GET /api/history/{profileId}/data | Compatibility bridge to the current latest clipboard item. It does not expose an older history list. |
POST /api/history/query, GET /api/history/statistics | Compatibility responses based on the current latest item, or an empty result when there is no item. |
PATCH /api/history/{type}/{hash}, DELETE /api/history/clear, DELETE /file | Compatibility-only. These requests are accepted so clients can continue their flow, but UniClipboard does not persist starred, pinned, deleted, clear, or folder-delete state through them. |
GET /api/time, GET /api/version, GET /version, / | Probe endpoints used by clients to confirm that the server is reachable. |
GET /api/sse/clipboard | UniClipboard-private push extension (SSE). Carries change signals only, never content; not part of the SyncClipboard protocol. |
GET /healthz | UniClipboard-private liveness probe. Public and constant-cost; not part of the SyncClipboard protocol, so older servers return 404. |
Upload behavior
Binary uploads are streamed into local staging. The daemon does not keep the whole uploaded file in memory.
PUT /file/{dataName} and multipart POST /api/history share a hard safety cap
of 10 GiB per uploaded file. A 1 GiB file is within the protocol cap, but it
still needs enough local disk space and time to finish over the LAN.
Metadata-only requests, such as PUT /SyncClipboard.json and URL-encoded
POST /api/history, remain capped at 16 MiB because they are small protocol
documents rather than file bodies.
Some Android clients send images as application/octet-stream. When that
happens, UniClipboard sniffs the file name and leading bytes so JPEG, PNG, GIF,
WEBP, BMP, TIFF, HEIC, and HEIF uploads remain image items instead of being
misreported as text or generic files.
UniClipboard-private extension (not part of the SyncClipboard protocol): a constant-cost liveness check for clients that have gone offline and need to discover when the listener is reachable again.
Public — no authentication. An Authorization header, if sent, is
ignored: no device lookup and no password verification happen. This is
deliberate. The endpoint performs no database, clipboard, blob, crypto,
or hashing work, so a probe costs a constant amount regardless of who
sends it. Requiring Basic Auth would instead spend a memory-hard
Argon2id verification (m=64MB, t=3, p=4) on every probe — the abuse
vector rather than a defense against it.
The response reveals only that a listener answers at this address —
something a TCP connect already reveals. It carries no version, device
identity, Space id, username, hostname, or clipboard information, and
it does not update the device's last_seen or any analytics state.
A 204 means the listener is live and accepting SyncClipboard requests.
It does not mean credentials are valid, the daemon is unlocked, or
the next sync will succeed — a successful probe should trigger one real
sync, which remains the authority on sync state.
Third-party and older SyncClipboard servers do not implement this path
and return 404; clients should treat that as "no health capability"
and fall back to a lower-frequency probe against the regular endpoints.
Deployment note: a reverse proxy must forward this to the same upstream
as /SyncClipboard.json and must not cache it (the server sends
Cache-Control: no-store). The daemon control API's GET /health is a
different endpoint on a different listener and must not stand in for it.
Response Body
curl -X GET "https://example.com/healthz"Compatibility probe. Returns a simple success string and does not read or write clipboard data.
Authorization
basicAuth Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.
In: header
Response Body
text/plain
curl -X GET "https://example.com""Server is running."Compatibility probe used by SyncClipboard clients.
Authorization
basicAuth Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.
In: header
Response Body
application/json
curl -X GET "https://example.com/api/time""2026-05-13T13:43:38Z"Compatibility probe used by SyncClipboard clients.
Authorization
basicAuth Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.
In: header
Response Body
application/json
curl -X GET "https://example.com/api/version""UniClipboard 1.0.0"Alias of GET /api/version.
Authorization
basicAuth Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.
In: header
Response Body
application/json
curl -X GET "https://example.com/version""UniClipboard 1.0.0"Returns the most recent clipboard entry on the desktop, translated into the SyncClipboard wire DTO. If the desktop has no clipboard item yet, the response is an empty Text document for compatibility.
For Image / File items, the binary payload is fetched separately via
GET /file/{dataName}.
Authorization
basicAuth Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.
In: header
Response Body
application/json
curl -X GET "https://example.com/SyncClipboard.json"{
"type": "Text",
"text": "hello from desktop",
"hasData": false,
"size": 18,
"hash": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
}Accepts a SyncClipboard wire DTO. The body is decoded, mapped into the application model, and applied through the inbound apply pipeline as if it came from a peer device.
Both camelCase and PascalCase keys are accepted (Type, HasData,
DataName, etc.) because the iOS Shortcuts client emits a mixed-case
body.
Decode failure produces 400 Bad Request; metadata payloads larger
than 16 MiB produce 413 Payload Too Large.
Authorization
basicAuth Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.
In: header
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
curl -X PUT "https://example.com/SyncClipboard.json" \ -H "Content-Type: application/json" \ -d '{ "type": "Text", "text": "hello from phone", "hasData": false, "size": 16 }'{
"contentId": "blake3v1:9b2cf964fa8c0b5a..."
}UniClipboard-private extension (not part of the SyncClipboard
protocol): a Server-Sent Events stream that signals when the desktop's
active clipboard changes, so a client can call
GET /SyncClipboard.json immediately instead of polling.
The stream carries signals only — never clipboard content. Events:
hello— sent once at connect time; payload{"server_time_ms": <int64>}. The client should pull once unconditionally on receiving it.update— the clipboard advanced; payload{"content_id": "blake3v1:<hex>", "server_time_ms": <int64>}.content_idis an opaque content identity for deduplication, not content itself.resync— the server dropped signal(s) for this connection; payload{"server_time_ms": <int64>}. The client should pull once.
A comment heartbeat (: ping) is written every 25 seconds; clients
should treat more than twice that with no bytes as a dead connection
and reconnect. There is no event replay on reconnect — a reconnecting
client pulls once unconditionally.
At most 2 concurrent streams per registered device; opening a third closes the oldest. The server also ends the stream when the device is revoked or its credentials are rotated.
Authorization
basicAuth Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.
In: header
Response Body
text/event-stream
curl -X GET "https://example.com/api/sse/clipboard""event: hello\ndata: {\"server_time_ms\":1750000000000}\n\nevent: update\ndata: {\"content_id\":\"blake3v1:9f8a...\",\"server_time_ms\":1750000012345}\n"Android-compatible inbound path for text, image, and file uploads.
URL-encoded requests are used for metadata-only entries. Multipart
requests may include a data file part; file bytes are streamed to
local staging and share the 10 GiB upload cap used by PUT /file/{dataName}.
Some Android clients send images as application/octet-stream. The
desktop sniffs the file name and leading bytes to recover common image
MIME types before applying the clipboard item.
Authorization
basicAuth Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.
In: header
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
curl -X POST "https://example.com/api/history" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d 'type=Text&hash=string'{
"hash": "string",
"type": "Text",
"text": "string",
"createTime": "2019-08-24T14:15:22Z",
"lastModified": "2019-08-24T14:15:22Z",
"lastAccessed": "2019-08-24T14:15:22Z",
"starred": true,
"pinned": true,
"size": 0,
"hasData": true,
"version": 0,
"isDeleted": true
}Compatibility bridge. Returns the current latest clipboard item as a one-element list, or an empty list when there is no current item. It is not a full paginated history query.
Authorization
basicAuth Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.
In: header
Response Body
application/json
curl -X POST "https://example.com/api/history/query"[
{
"hash": "string",
"type": "Text",
"text": "string",
"createTime": "2019-08-24T14:15:22Z",
"lastModified": "2019-08-24T14:15:22Z",
"lastAccessed": "2019-08-24T14:15:22Z",
"starred": true,
"pinned": true,
"size": 0,
"hasData": true,
"version": 0,
"isDeleted": true
}
]Compatibility bridge. Statistics are computed from the current latest clipboard item, or all zeros when there is no current item. It is not a full history database summary.
Authorization
basicAuth Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.
In: header
Response Body
application/json
curl -X GET "https://example.com/api/history/statistics"{
"totalCount": 0,
"starredCount": 0,
"deletedCount": 0,
"activeCount": 0,
"totalFileSizeMb": 0
}Compatibility bridge to the current latest clipboard item. For image and file records, UniClipboard can satisfy a same-type request even when the client's uploaded hash differs from the server-recomputed hash.
Authorization
basicAuth Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.
In: header
Path Parameters
SyncClipboard profile id in {type}-{hash} form, for example Image-ABCDEF.
Response Body
application/json
curl -X GET "https://example.com/api/history/Image-0B13A2265544DE3C8C1286E4B854D39833A49BDAA3F82114AE19F55B7F08FBB2"{
"hash": "string",
"type": "Text",
"text": "string",
"createTime": "2019-08-24T14:15:22Z",
"lastModified": "2019-08-24T14:15:22Z",
"lastAccessed": "2019-08-24T14:15:22Z",
"starred": true,
"pinned": true,
"size": 0,
"hasData": true,
"version": 0,
"isDeleted": true
}Compatibility bridge to GET /file/{dataName} for the current latest
image or file item. It does not expose older historical payloads.
Authorization
basicAuth Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.
In: header
Path Parameters
SyncClipboard profile id in {type}-{hash} form, for example Image-ABCDEF.
Response Body
application/octet-stream
curl -X GET "https://example.com/api/history/Image-0B13A2265544DE3C8C1286E4B854D39833A49BDAA3F82114AE19F55B7F08FBB2/data""string"Compatibility-only route. It returns the current matching record, but UniClipboard does not persist starred, pinned, deleted, or other history-state mutations through this endpoint.
Authorization
basicAuth Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.
In: header
Path Parameters
SyncClipboard item type.
"Text" | "Image" | "File" | "Group"Item hash, or an embedded {type}-{hash} profile id.
Response Body
application/json
curl -X PATCH "https://example.com/api/history/Text/string"{
"hash": "string",
"type": "Text",
"text": "string",
"createTime": "2019-08-24T14:15:22Z",
"lastModified": "2019-08-24T14:15:22Z",
"lastAccessed": "2019-08-24T14:15:22Z",
"starred": true,
"pinned": true,
"size": 0,
"hasData": true,
"version": 0,
"isDeleted": true
}Compatibility-only route. The request is accepted so clients can continue their flow, but UniClipboard does not clear the real clipboard history through this endpoint.
Authorization
basicAuth Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.
In: header
Response Body
application/json
curl -X DELETE "https://example.com/api/history/clear"{
"deleted": 0
}Compatibility-only route. It confirms that the /file folder endpoint
exists, but actual file bytes are handled by /file/{dataName}.
Authorization
basicAuth Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.
In: header
Response Body
curl -X GET "https://example.com/file"Compatibility-only route. The request is accepted so clients can continue their flow, but UniClipboard does not delete staged clipboard payloads through this endpoint.
Authorization
basicAuth Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.
In: header
Response Body
curl -X DELETE "https://example.com/file"Returns the raw bytes of the most recent Image / File clipboard entry
whose dataName matches. The Content-Type header reflects the
sniffed MIME type.
Authorization
basicAuth Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.
In: header
Path Parameters
Filename advertised by the matching SyncClipboard.json document.
Used to look up the staged payload for Image / File clipboard items.
Response Body
application/octet-stream
curl -X GET "https://example.com/file/clipboard.png""string"Compatibility HEAD request for the same resource as GET /file/{dataName}.
It is useful for clients that probe the payload before downloading it.
Authorization
basicAuth Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.
In: header
Path Parameters
Filename advertised by the matching SyncClipboard.json document.
Used to look up the staged payload for Image / File clipboard items.
Response Body
curl -X HEAD "https://example.com/file/clipboard.png"Accepts the raw bytes of an image or file. The desktop stages the bytes
into the inbound mobile buffer, where they will be paired with a
subsequent PUT /SyncClipboard.json describing the same dataName.
MIME handling
Some clients send application/octet-stream or omit Content-Type.
The desktop sniffs the file's magic bytes, and falls back to the
extension, to recover an accurate image/* MIME so desktop clipboard
APIs can render the image correctly. If the sniff fails, the original
Content-Type is kept.
Recognised image magics and extensions: JPEG, PNG, GIF, WEBP, BMP, TIFF, HEIC, and HEIF.
Maximum body size is 10 GiB. The body is streamed to local staging; it is not buffered fully in memory.
Authorization
basicAuth Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.
In: header
Path Parameters
Filename advertised by the matching SyncClipboard.json document.
Used to look up the staged payload for Image / File clipboard items.
Request Body
application/octet-stream
Raw bytes. Set Content-Type to the actual MIME of the upload
(image/png, image/jpeg, application/pdf, etc.) when known. If
application/octet-stream is sent for image uploads, the desktop
will sniff the magic bytes and recover the correct image/* MIME.
TypeScript Definitions
Use the request body type in TypeScript.
binaryResponse Body
curl -X PUT "https://example.com/file/clipboard.png" \ -H "Content-Type: application/octet-stream" \ -d 'string'