This document describes protocol version 1 as implemented by the current
server.
GET /api/v1/health, POST /api/v1/auth/login, and the device refresh/revoke
routes do not require a session Bearer header. Login accepts a Lazync account
username and password and returns an opaque session token. Device refresh and
revoke authenticate with their opaque device secret in the JSON body. Every
other /api/v1/* endpoint requires a session token:
Authorization: Bearer SESSION_TOKENSession tokens are random, expire after the configured idle lifetime, and are not persisted across a server restart. Device tokens are long-lived and revocable; only their SHA-256 hashes are persisted by the server. Do not place a password, device token, or session token in a URL, log, or configuration file.
Use HTTPS for every non-loopback connection. A reverse proxy may terminate TLS
only when the backend server listens on loopback. API responses include
X-Request-ID; include this value when reporting a server error.
The Web UI uses the same accounts through a form login and an HttpOnly,
SameSite=Strict session cookie. Set web_secure_cookie=1 when HTTPS is
terminated by a reverse proxy, because the backend listener cannot otherwise
detect the external scheme.
API errors use JSON:
{
"ok": false,
"code": "not_found",
"message": "File not found.",
"request_id": "a1b2c3..."
}Internal errors return a generic message. Details and the same request ID are written to the server log.
Common status codes are 400, 401, 403, 404, 405, 409, 413,
422, 429, and 500.
GET /api/v1/healthNo authentication is required. Example response:
{
"status": "ok",
"app": "Lazync",
"version": "0.1.0-dev",
"protocol_version": 1,
"instance_id": "0123456789abcdef0123456789abcdef"
}instance_id is a stable, non-secret identity stored in the managed library.
Clients use it to recognize one server behind different LAN and WAN URLs. The
response intentionally does not expose storage paths or configuration.
POST /api/v1/auth/login
Content-Type: application/json
{"username":"alice","password":"correct horse battery staple"}Successful response:
{
"ok": true,
"session_token": "OPAQUE_RANDOM_VALUE",
"expires_unix": 1785600000,
"instance_id": "0123456789abcdef0123456789abcdef",
"account_id": "fedcba9876543210fedcba9876543210",
"username": "alice",
"administrator": false,
"server_version": "0.1.0-dev",
"protocol_version": 1
}Invalid credentials return 401 with the same message for unknown users and
wrong passwords. Five failures for one normalized username and source address
within five minutes return 429 rate_limited and Retry-After: 300.
Passwords created or changed in the Web UI must contain at least 8 valid UTF-8 characters and no more than 512 UTF-8 bytes. An 8-character ASCII password meets the minimum.
GET /api/v1/session
Authorization: Bearer SESSION_TOKENValidates and refreshes the idle session and returns its account context:
{
"ok": true,
"server_version": "0.1.0-dev",
"protocol_version": 1,
"https": true,
"instance_id": "0123456789abcdef0123456789abcdef",
"account_id": "fedcba9876543210fedcba9876543210",
"username": "alice",
"administrator": false,
"expires_unix": 1785600000
}https describes the server's own listener. It is false when TLS terminates
at a reverse proxy.
End a session explicitly with:
POST /api/v1/auth/logout
Authorization: Bearer SESSION_TOKENSessions and device credentials are invalidated when an administrator changes the account password, disables the account, or deletes it. Permission changes apply to existing sessions on their next request.
Create a persistent device credential after password login:
POST /api/v1/auth/device
Authorization: Bearer SESSION_TOKEN
Content-Type: application/json
{"device_name":"WORKSTATION-01"}The optional UTF-8 device name is limited to 128 bytes. A user may have up to
32 devices; issuing another removes the least recently used device and any
short-lived sessions issued from it. Successful
creation returns HTTP 201:
{
"ok": true,
"instance_id": "0123456789abcdef0123456789abcdef",
"account_id": "fedcba9876543210fedcba9876543210",
"device_id": "11111111111111111111111111111111",
"device_token": "OPAQUE_LONG_LIVED_SECRET"
}The token is returned only once. A client stores it in the operating system's credential store, never in its INI file.
Obtain a new memory-only session after a client or server restart:
POST /api/v1/auth/device/refresh
Content-Type: application/json
{"device_id":"11111111111111111111111111111111","device_token":"OPAQUE_LONG_LIVED_SECRET"}The response contains the same session and identity fields as password login.
An unknown, invalid, disabled, or revoked device returns 401.
Explicit sign-out revokes the device credential:
POST /api/v1/auth/device/revoke
Authorization: Bearer SESSION_TOKEN
Content-Type: application/json
{"device_id":"11111111111111111111111111111111","device_token":"OPAQUE_LONG_LIVED_SECRET"}The Bearer header is optional; when present, that session is also removed.
A matching device ID with the wrong token returns 401; an already absent
device is treated as successfully revoked. Successful revocation also removes
short-lived sessions previously issued from that device credential. Clients
must delete their local credential only after the server responds, then retain
local files while marking the folder unbound.
Administrators have read/write access to the whole library. A regular account
has zero or more directory grants. A grant applies to the named relative
directory and all descendants; * denotes the whole library. Read-only grants
allow listing, download, path validation, and version history. Read/write
grants additionally allow upload, move, delete, and version restore.
File and directory lists are filtered before pagination, so they contain only
accessible paths and report an accessible total. Directory listings also
include existing ancestors needed to reach a granted subtree. Direct access
outside the account's grants returns 403 forbidden. A move requires write
access to both source and destination.
GET /api/v1/path/validate?path=Documents%2Freadme.txt
Authorization: Bearer SESSION_TOKENPath rules:
- Relative paths only; normalized separator is
/. - No absolute paths or parent (
..) segments. - No control characters or Windows-incompatible characters/endings.
- No Windows reserved device names.
- Maximum relative path: 2048 UTF-8 bytes.
- Maximum segment: 240 UTF-8 bytes.
- Maximum depth: 64 segments.
- Maximum resolved physical path: 3800 bytes.
Invalid paths receive HTTP 400 and a stable path error code.
GET /api/v1/files/list?offset=0&limit=200&include_hash=0
Authorization: Bearer SESSION_TOKENParameters:
| Name | Default | Limit | Meaning |
|---|---|---|---|
offset |
0 |
non-negative integer | Sorted result offset |
limit |
200 |
1..1000 |
Page size |
include_hash |
0 |
0, 1, true |
Include SHA-256; adds file I/O |
Example:
{
"ok": true,
"offset": 0,
"limit": 200,
"total": 1,
"sync_scope_version": 1,
"read_roots": ["*"],
"files": [
{
"path": "Documents/readme.txt",
"size": 125,
"modified_unix": 1784304000
}
]
}The server aborts the listing if the configured max_list_entries or scan
safety limit is exceeded.
The first page after a server start or library mutation rebuilds a shared, sorted file/directory path cache with one tree walk. Subsequent pages use index slices, and permission-filtered pages use merged path ranges. Clients should still page sequentially and must not assume that an offset snapshot remains stable across concurrent writes.
sync_scope_version: 1 declares that the paginated result is complete for the
listed read_roots. A regular account receives its normalized readable grant
roots; an administrator receives *. Sync clients use this scope to distinguish
a deleted file from a file that merely became invisible after a permission
change. They still confirm a missing path through the metadata endpoint before
deleting local content.
GET /api/v1/directories/entries?path=Documents
Authorization: Bearer SESSION_TOKENpath is optional; omit it to browse the file-library root. The response
contains only the immediate children of that directory, sorted with folders
before files. Empty folders therefore appear without requiring a placeholder
file.
{
"ok": true,
"path": "Documents",
"total": 2,
"entries": [
{
"path": "Documents/Empty folder",
"name": "Empty folder",
"type": "directory",
"size": 0,
"modified_unix": 1784303900
},
{
"path": "Documents/readme.txt",
"name": "readme.txt",
"type": "file",
"size": 125,
"modified_unix": 1784304000
}
]
}The requested directory and every returned entry are permission-filtered. A
directory needed as an ancestor of a granted path is visible, but files outside
the account's read grants and unrelated sibling directories are not. Browsing
an inaccessible directory returns HTTP 403; a missing directory returns
HTTP 404. The configured max_list_entries applies to each directory.
GET /api/v1/directories/list?offset=0&limit=200
Authorization: Bearer SESSION_TOKENoffset and limit use the same bounds as the file list. Results contain all
live directories, including empty directories, in sorted relative-path order:
{
"ok": true,
"offset": 0,
"limit": 200,
"total": 2,
"directories": [
{"path": "Documents"},
{"path": "Documents/Empty folder"}
]
}Permission filtering happens before pagination. A regular account receives its granted directories, their descendants, and existing ancestors needed to construct the visible tree; sibling and unrelated directories are omitted.
GET /api/v1/changes?cursor=EPOCH%3ASEQUENCE&limit=500
Authorization: Bearer SESSION_TOKENlimit defaults to 500 and accepts 1..1000. A cursor is opaque to clients
even though the current representation is <32-hex epoch>:<sequence>.
{
"ok": true,
"cursor": "0123456789abcdef0123456789abcdef:42",
"has_more": false,
"reset_required": false,
"journal_healthy": true,
"changes": [
{
"sequence": 42,
"path": "Documents/readme.txt",
"entry_type": "file",
"action": "upsert"
}
]
}entry_type is file or directory; action is upsert or delete. A file
move is represented by a delete for the old path followed by an upsert for the
new path. Events outside the account's visible read scope are omitted, while
the returned cursor still advances past them.
An absent, malformed, foreign, future, or expired cursor returns HTTP 200
with reset_required: true, an empty change list, and the current cursor. The
client must take a complete permission-filtered file and directory snapshot,
then continue from that cursor. Permission changes are also reconciled through
that periodic snapshot because they do not create file-change events.
The append-only journal compacts after 1,000,000 records and retains the newest
500,000. journal_healthy: false requires complete snapshot fallback; clients
must rate-limit that fallback rather than repeatedly requesting full lists.
POST /api/v1/directories/create?path=Documents%2FEmpty%20folder
Authorization: Bearer SESSION_TOKEN
Content-Type: application/json
{}The caller needs write access to path, and its parent directory must already
exist. Creation is idempotent: a new directory returns 201 with
"created": true; an existing directory returns 200 with "created": false.
A file or unsupported filesystem entry at the same path returns 409.
{
"ok": true,
"path": "Documents/Empty folder",
"created": true
}Protocol version 1 has no public directory-delete API. The Web UI may delete an empty directory, which emits a change event. The Windows client removes the corresponding local directory only if it was synchronized and is still empty.
GET /api/v1/files/metadata?path=Documents%2Freadme.txt
Authorization: Bearer SESSION_TOKENReturns metadata without returning file content:
{
"ok": true,
"path": "Documents/readme.txt",
"size": 125,
"sha256": "LOWERCASE_SHA256"
}The caller needs read access. An inaccessible path returns 403; a missing
live file returns 404. The sync client uses this distinction to confirm a
server-side deletion after a path disappears from a complete scoped listing.
GET /api/v1/files/content?path=Documents%2Freadme.txt
Authorization: Bearer SESSION_TOKENThe response body is application/octet-stream. The ETag is the quoted
lowercase SHA-256 hash of the returned content and Accept-Ranges is bytes.
Conditional download:
If-None-Match: "CURRENT_SHA256"Returns 304 if unchanged.
Resumable download uses one explicit byte range and pins the source version:
Range: bytes=8388608-
If-Match: "EXPECTED_SHA256"The server returns 206, Content-Range, the same ETag, and the requested
suffix. Multiple ranges and suffix-only ranges are not supported; malformed or
unsatisfiable ranges return 416. A changed If-Match version returns 412.
The client appends only a valid 206 response whose range start and ETag match
its local partial file, then verifies final size and SHA-256 before atomic
installation.
Start or recover an upload session:
POST /api/v1/uploads/start
Authorization: Bearer SESSION_TOKEN
Content-Type: application/json
{
"path": "Documents/archive.bin",
"size": 12582912,
"sha256": "LOWERCASE_SHA256",
"expected_current_hash": "!absent"
}expected_current_hash may be empty for an unconditional write, !absent for
create-only behavior, or the current lowercase SHA-256 for an overwrite. A new
session returns 201; repeating the same account/path/size/hash/precondition
returns the existing session with 200:
{
"ok": true,
"created": true,
"upload_id": "11111111111111111111111111111111",
"path": "Documents/archive.bin",
"size": 12582912,
"sha256": "LOWERCASE_SHA256",
"chunk_size": 8388608,
"received_bytes": 0,
"created_unix": 1785600000,
"updated_unix": 1785600000
}Send sequential chunks at the acknowledged offset:
PUT /api/v1/uploads/chunk?id=UPLOAD_ID&offset=0&hash=CHUNK_SHA256
Authorization: Bearer SESSION_TOKEN
Content-Type: application/octet-stream
...one chunk...The hash is required. A hash mismatch returns 422; an offset other than
received_bytes returns 409; a chunk above configured max_chunk_bytes or
beyond the declared file size returns 413. Each accepted chunk and manifest
is flushed before its new offset is acknowledged.
Inspect, commit, or cancel the account-owned session:
GET /api/v1/uploads/status?id=UPLOAD_ID
POST /api/v1/uploads/commit?id=UPLOAD_ID
POST /api/v1/uploads/cancel?id=UPLOAD_IDStatus returns the same fields as start. Commit requires all declared bytes,
verifies the complete SHA-256 and optimistic precondition, then uses the normal
versioned atomic file commit. It returns file metadata with 201 for a create
or 200 for an overwrite. Cancel deletes the manifest and partial content.
Sessions and partial bytes survive a server restart. Idle sessions expire after seven days and the server admits at most 1,024 active sessions. If a commit succeeded but its response was lost, a repeated start recognizes the matching live size and SHA-256, reports all bytes received, and allows a no-op commit without retransmitting the file.
PUT /api/v1/files/content?path=Documents%2Freadme.txt&hash=NEW_SHA256
Authorization: Bearer SESSION_TOKEN
Content-Type: application/octet-stream
...whole file body...PUTis the only accepted method on this compatibility endpoint.hashis optional. If supplied and the body differs, the server returns422 hash_mismatchwithout changing the current file.- A successful create returns
201; an overwrite returns200. - The response and
ETagcontain the committed SHA-256. - The request body must not exceed configured
max_upload_bytes. - The listener rejects connections above configured
max_connections.
Optimistic concurrency options:
If-Match: "EXPECTED_CURRENT_SHA256"Writes only if the current content has that hash. If-Match: * requires the
file to exist. The value must be one quoted or unquoted hexadecimal SHA-256 (or
*); empty, malformed, or multi-value conditions return 400.
If-None-Match: *Creates only if the path is absent. A failed precondition returns 409 and
does not change the file. Upload requests must not combine If-Match and
If-None-Match; malformed combinations return 400 before content is
committed.
POST /api/v1/files/move
Authorization: Bearer SESSION_TOKEN
Content-Type: application/x-www-form-urlencoded
source_path=Documents%2Fold.txt&target_path=Documents%2Fnew.txtThe two parameters may also be passed in the query string. The target must be absent. A move preserves the immutable file identity and all version history.
DELETE /api/v1/files/content?path=Documents%2Freadme.txt
Authorization: Bearer SESSION_TOKENThe server creates a historical version, moves the live content into internal
trash, and returns an opaque trash_id. There is no public trash restore API in
protocol version 1.
GET /api/v1/files/versions?path=Documents%2Freadme.txt
Authorization: Bearer SESSION_TOKENReturns sorted opaque version IDs for the file's immutable identity.
POST /api/v1/files/versions/restore
Authorization: Bearer SESSION_TOKEN
Content-Type: application/x-www-form-urlencoded
path=Documents%2Freadme.txt&version_id=VERSION_IDParameters may also be in the query string. If a current file exists, it is versioned before the selected historical content becomes current. The response contains the restored SHA-256.
GET /web/login?lang=en|zh
POST /web/login?lang=en|zh
GET /web/lazync.js
POST /web/logout?lang=en|zh
GET /web/files?dir=...&lang=en|zh
GET /web/files/download?path=...&lang=en|zh
POST /web/files/upload?lang=en|zh
POST /web/files/create-directory?lang=en|zh
POST /web/files/delete?lang=en|zh
GET /web/files/history?path=...&lang=en|zh
POST /web/files/restore-version?lang=en|zh
GET /web/admin/users?lang=en|zh
POST /web/admin/users/create?lang=en|zh
GET /web/admin/users/edit?username=...&lang=en|zh
POST /web/admin/users/update?lang=en|zh
POST /web/admin/users/delete?lang=en|zh
All routes except the login page and its static script require a valid session
cookie. Administrator user-management routes require an administrator account.
Every authenticated POST contains a
per-session CSRF token; login has a separate process-local CSRF token.
The account named by security.admin_username is root-managed: its edit,
update, and delete routes return 409, and its controls are omitted from the
user list. Change that account in the server INI and restart instead.
The file page browses one directory at a time; an omitted dir denotes the
library root. Directory ancestors needed to reach an account's grants are
visible without granting access to siblings. Upload uses
multipart/form-data fields upload_file, directory, csrf_token, and an
optional overwrite=1. Existing files are rejected unless overwrite is
explicit; replacement creates a historical version. Directory creation uses
name and directory. Deletion uses path, directory, and
kind=file|directory; files follow the normal version/trash path and a
directory must be empty.
- Desktop resumable transfers accept files through configured
max_file_bytes, which defaults to 2,000,000,000,000 bytes. Each upload request is bounded bymax_chunk_bytes, default 8 MiB. - Compatibility whole-file API and Web multipart requests remain buffered by
FCL-Web and are separately bounded by
max_upload_bytes, default 256 MiB. Multipart framing counts toward that request-body limit. - Management-client text responses are capped at 8 MiB.
- Short sessions are server-memory state, but bound clients obtain a new one from their persistent revocable device credential after a server restart. There is not yet a device/session inventory or selective revocation UI.
- The change feed has path-level tombstones but no batch mutation API. The client periodically reconciles full scoped lists to cover permission changes, expired cursors, watcher overflow, and offline edits. Local deletion and rename propagation remain disabled; remote deletions are applied only when the local item still matches synchronized state.
- The server is intended to be the exclusive writer of its managed library.