UniClipboard

Mobile LAN API

SyncClipboard-compatible HTTP endpoints exposed by the desktop daemon for mobile clients.

Edit on GitHub

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 pulls GET /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 groupStatus
GET /SyncClipboard.json, PUT /SyncClipboard.jsonReal 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/historyReal Android-compatible inbound path for text, image, and file uploads.
GET /api/history/{profileId}, GET /api/history/{profileId}/dataCompatibility bridge to the current latest clipboard item. It does not expose an older history list.
POST /api/history/query, GET /api/history/statisticsCompatibility 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 /fileCompatibility-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/clipboardUniClipboard-private push extension (SSE). Carries change signals only, never content; not part of the SyncClipboard protocol.
GET /healthzUniClipboard-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.

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

On this page