移动端 LAN API
桌面守护进程对外暴露的、SyncClipboard 兼容的 HTTP 接口,供移动端客户端使用。
桌面守护进程在 LAN 上提供一个体量很小、与 SyncClipboard 协议兼容的 HTTP 服务, 让已配对的移动设备 —— iOS 快捷指令、Android 或任意第三方 SyncClipboard 客户端 —— 都可以读取与推送剪贴板内容。
所有接口都挂在 根路径。主要 SyncClipboard 路径是
/SyncClipboard.json 和 /file/{dataName};兼容 Android 客户端时还会用到
/api/history/*、/api/time、/api/version、/version、/ 和 /file。
此外还有两个 UniClipboard 私有扩展,都不属于 SyncClipboard 协议,第三方
客户端可以忽略:
GET /api/sse/clipboard(Server-Sent Events):桌面剪贴板更新时向订阅的 客户端推送信号,客户端收到后再走GET /SyncClipboard.json拉取,替代周期 轮询。GET /healthz:常数成本的存活探测,供掉线的客户端快速发现监听器何时恢复。
除 GET /healthz 外,每个接口都会通过 HTTP Basic 认证 校验。用户名 /
密码由桌面端在配对流程中签发,且与一台已登记的移动设备一一绑定。
GET /healthz 公开且无需鉴权,这是有意设计:它只回一个空的 204,不含版本、
设备、用户名或剪贴板信息 —— 只暴露「这个地址上有个监听器在应答」,而这一点
TCP 连接本身就已经暴露了。反过来,让每次探测都跑一遍 Argon2id 密码校验才是
真正的滥用面。
本接口属于 移动端伴侣 路径,独立于桌面 ↔ 桌面同步所走的 iroh P2P 传输,运行在明文 HTTP 之上,自身 没有 NAT 打洞、没有加密中继。离开 LAN 的客户端无法直接触达此服务 —— 用 无头 server 节点(公网 HTTPS)或 Tailscale / VPN overlay 把它挡在前面,即可跨网络访问。相关取舍见 配对与同步 — 移动端伴侣。
配对流程见 配对与同步 — 移动端伴侣, 以及桌面端的 设备 → 移动端同步 面板。
兼容边界
UniClipboard 暴露的是当前剪贴板同步和 Android 客户端兼容所需的 SyncClipboard 服务端子集,不是完整的 SyncClipboard 历史服务端。
| 接口组 | 当前状态 |
|---|---|
GET /SyncClipboard.json、PUT /SyncClipboard.json | 真实读取 / 写入当前最新剪贴板元数据。 |
GET /file/{dataName}、PUT /file/{dataName}、HEAD /file/{dataName} | 真实下载 / 上传图片和文件内容。 |
POST /api/history | 真实接入 Android 兼容的文字、图片、文件上传入口。 |
GET /api/history/{profileId}、GET /api/history/{profileId}/data | 兼容桥接到当前最新剪贴板内容,不提供更早的历史记录列表。 |
POST /api/history/query、GET /api/history/statistics | 基于当前最新一条返回兼容结果;没有当前内容时返回空结果。 |
PATCH /api/history/{type}/{hash}、DELETE /api/history/clear、DELETE /file | 仅用于兼容客户端流程。服务端会接收请求,但不会通过这些接口持久化标星、置顶、删除、清空或文件夹删除状态。 |
GET /api/time、GET /api/version、GET /version、/ | 客户端用于确认服务在线的探测接口。 |
GET /api/sse/clipboard | UniClipboard 私有推送扩展(SSE),只送变更信号不送内容;不属于 SyncClipboard 协议。 |
GET /healthz | UniClipboard 私有存活探测,公开且常数成本;不属于 SyncClipboard 协议,旧服务端返回 404。 |
上传行为
二进制上传会流式写入本地暂存区。桌面守护进程不会把整个上传文件一次性放进内存。
PUT /file/{dataName} 和 multipart POST /api/history 共享每个上传文件
10 GiB 的硬安全上限。1 GiB 文件在协议上允许,但仍需要本机有足够磁盘空间,
也需要局域网传输有足够时间完成。
PUT /SyncClipboard.json 和 URL-encoded POST /api/history 这类只传元数据的
请求仍限制为 16 MiB,因为它们只是很小的协议文档,不是文件正文。
部分 Android 客户端会把图片按 application/octet-stream 发送。遇到这种情况时,
UniClipboard 会根据文件名和文件开头字节识别图片格式,让 JPEG、PNG、GIF、WEBP、
BMP、TIFF、HEIC 和 HEIF 上传继续保持为图片,而不是被误报成文本或普通文件。
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'