UniClipboard
参考

移动端 LAN API

桌面守护进程对外暴露的、SyncClipboard 兼容的 HTTP 接口,供移动端客户端使用。

在 GitHub 上编辑

桌面守护进程在 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.jsonPUT /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/queryGET /api/history/statistics基于当前最新一条返回兼容结果;没有当前内容时返回空结果。
PATCH /api/history/{type}/{hash}DELETE /api/history/clearDELETE /file仅用于兼容客户端流程。服务端会接收请求,但不会通过这些接口持久化标星、置顶、删除、清空或文件夹删除状态。
GET /api/timeGET /api/versionGET /version/客户端用于确认服务在线的探测接口。
GET /api/sse/clipboardUniClipboard 私有推送扩展(SSE),只送变更信号不送内容;不属于 SyncClipboard 协议。
GET /healthzUniClipboard 私有存活探测,公开且常数成本;不属于 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 上传继续保持为图片,而不是被误报成文本或普通文件。

GET
/healthz

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"
Empty
Empty
Empty
GET
/

Compatibility probe. Returns a simple success string and does not read or write clipboard data.

Authorization

basicAuth
AuthorizationBasic <token>

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."
Empty
GET
/api/time

Compatibility probe used by SyncClipboard clients.

Authorization

basicAuth
AuthorizationBasic <token>

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"
Empty
GET
/api/version

Compatibility probe used by SyncClipboard clients.

Authorization

basicAuth
AuthorizationBasic <token>

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"
Empty
GET
/version

Alias of GET /api/version.

Authorization

basicAuth
AuthorizationBasic <token>

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"
Empty
GET
/SyncClipboard.json

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
AuthorizationBasic <token>

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"
}

Empty
Empty
PUT
/SyncClipboard.json

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
AuthorizationBasic <token>

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..."
}
Empty
Empty
Empty
Empty
GET
/api/sse/clipboard

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_id is 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
AuthorizationBasic <token>

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"
Empty
POST
/api/history

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
AuthorizationBasic <token>

Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.

In: header

Request Body

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
}
Empty
Empty
Empty
Empty
POST
/api/history/query

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
AuthorizationBasic <token>

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
  }
]
Empty
Empty
GET
/api/history/statistics

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
AuthorizationBasic <token>

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
}
Empty
Empty
GET
/api/history/{profileId}

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
AuthorizationBasic <token>

Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.

In: header

Path Parameters

profileId*string

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
}
Empty
Empty
Empty
Empty
GET
/api/history/{profileId}/data

Compatibility bridge to GET /file/{dataName} for the current latest image or file item. It does not expose older historical payloads.

Authorization

basicAuth
AuthorizationBasic <token>

Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.

In: header

Path Parameters

profileId*string

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"
Empty
Empty
Empty
Empty
PATCH
/api/history/{type}/{hash}

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
AuthorizationBasic <token>

Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.

In: header

Path Parameters

type*string

SyncClipboard item type.

Value in"Text" | "Image" | "File" | "Group"
hash*string

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
}
Empty
Empty
Empty
Empty
DELETE
/api/history/clear

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
AuthorizationBasic <token>

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
}
Empty
GET
/file

Compatibility-only route. It confirms that the /file folder endpoint exists, but actual file bytes are handled by /file/{dataName}.

Authorization

basicAuth
AuthorizationBasic <token>

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"
Empty
Empty
DELETE
/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
AuthorizationBasic <token>

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"
Empty
Empty
GET
/file/{dataName}

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
AuthorizationBasic <token>

Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.

In: header

Path Parameters

dataName*string

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"
Empty
Empty
Empty
HEAD
/file/{dataName}

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
AuthorizationBasic <token>

Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.

In: header

Path Parameters

dataName*string

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"
Empty
Empty
Empty
Empty
PUT
/file/{dataName}

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
AuthorizationBasic <token>

Username and password are minted on the desktop during device pairing and are bound to a single mobile device record.

In: header

Path Parameters

dataName*string

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.

body*file
Formatbinary

Response Body

curl -X PUT "https://example.com/file/clipboard.png" \  -H "Content-Type: application/octet-stream" \  -d 'string'
Empty
Empty
Empty
Empty

本页目录